Browse Source

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 years ago
parent
commit
56a00d2f9f
68 changed files with 3113 additions and 2461 deletions
  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
         python manage.py makemigrations --check
       displayName: 'CR-QC: Check migrations'
       displayName: 'CR-QC: Check migrations'
 
 
-    - pwsh: ./ci/run-flake8.ps1
+    - script: black --check .
+      displayName: 'CR-QC: Black'
+
+    - script: flake8 .
       displayName: 'CR-QC: Flake8'
       displayName: 'CR-QC: Flake8'
 
 
   - job: codecov
   - 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 = [
 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):
     def set_access_token(self, site=None):
         site = site or Site.objects.get(is_default_site=True)
         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:
         if self.access_token:
             self.set_base_url()
             self.set_base_url()
             self.is_active = True
             self.is_active = True
@@ -24,7 +26,7 @@ class MailchimpApi:
         """
         """
         The base url for the mailchimip api is dependent on the api key.
         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)
         self.base_url = self.proto_base_url.format(datacenter)
 
 
     def default_headers(self):
     def default_headers(self):
@@ -41,18 +43,28 @@ class MailchimpApi:
         return json_response
         return json_response
 
 
     def get_merge_fields_for_list(self, list_id):
     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)
         json_response = self._get(endpoint)
         return json_response
         return json_response
 
 
     def get_interest_categories_for_list(self, list_id):
     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)
         json_response = self._get(endpoint)
         return json_response
         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)
         json_response = self._get(endpoint)
         return json_response
         return json_response
 
 
@@ -65,12 +77,16 @@ class MailchimpApi:
         auth = auth or self.default_auth()
         auth = auth or self.default_auth()
         headers = headers or self.default_headers()
         headers = headers or self.default_headers()
         full_url = "{0}{1}".format(self.base_url, endpoint)
         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()
         return r.json()
 
 
     def _post(self, endpoint, data={}, auth=None, headers=None, **kwargs):
     def _post(self, endpoint, data={}, auth=None, headers=None, **kwargs):
         auth = auth or self.default_auth()
         auth = auth or self.default_auth()
         headers = headers or self.default_headers()
         headers = headers or self.default_headers()
         full_url = "{0}{1}".format(self.base_url, endpoint)
         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()
         return r.json()

+ 3 - 3
coderedcms/apps.py

@@ -2,8 +2,8 @@ from django.apps import AppConfig
 
 
 
 
 class CoderedcmsConfig(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
     # TODO: At some point in the future, change this to BigAutoField and create
     # the corresponding migration for concrete models in coderedcms.
     # 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:
 if CURRENT_PYTHON < REQUIRED_PYTHON:
     sys.stderr.write(
     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)
     sys.exit(1)
 
 
@@ -20,24 +22,25 @@ class CreateProject(TemplateCommand):
     """
     """
     Based on django.core.management.startproject
     Based on django.core.management.startproject
     """
     """
+
     help = "Creates the directory structure for a new Wagtail CRX project."
     help = "Creates the directory structure for a new Wagtail CRX project."
     missing_args_message = "You must provide a project name."
     missing_args_message = "You must provide a project name."
 
 
     def add_arguments(self, parser):
     def add_arguments(self, parser):
         parser.add_argument(
         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(
         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)
         super().add_arguments(parser)
 
 
     def handle(self, **options):
     def handle(self, **options):
         # pop standard args
         # 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.
         # Make sure given name is not already in use by another python package/module.
         try:
         try:
@@ -45,68 +48,76 @@ class CreateProject(TemplateCommand):
         except ImportError:
         except ImportError:
             pass
             pass
         else:
         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.
         # 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
         # Handle custom template logic
         import coderedcms
         import coderedcms
+
         codered_path = os.path.dirname(coderedcms.__file__)
         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(
         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,
         # Check if provided template is built-in to coderedcms,
         # otherwise, do not change it.
         # otherwise, do not change it.
         if os.path.isdir(template_path):
         if os.path.isdir(template_path):
-            options['template'] = template_path
+            options["template"] = template_path
 
 
         # Treat these files as Django templates to render the boilerplate.
         # 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
         # Set options
         message = "Creating a Wagtail CRX project called %(project_name)s"
         message = "Creating a Wagtail CRX project called %(project_name)s"
 
 
-        if options.get('sitename'):
+        if options.get("sitename"):
             message += " for %(sitename)s"
             message += " for %(sitename)s"
         else:
         else:
-            options['sitename'] = project_name
+            options["sitename"] = project_name
 
 
-        if options.get('domain'):
+        if options.get("domain"):
             message += " (%(domain)s)"
             message += " (%(domain)s)"
             # Stip protocol out of domain if it is present.
             # 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.
             # 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:
             else:
-                options['domain_nowww'] = options['domain']
+                options["domain_nowww"] = options["domain"]
         else:
         else:
-            options['domain'] = 'localhost'
-            options['domain_nowww'] = options['domain']
+            options["domain"] = "localhost"
+            options["domain_nowww"] = options["domain"]
 
 
         # Add additional custom options to the context.
         # Add additional custom options to the context.
-        options['coderedcms_release'] = coderedcms.release
+        options["coderedcms_release"] = coderedcms.release
 
 
         # Print a friendly message
         # 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
         # Run command
-        super().handle('project', project_name, target, **options)
+        super().handle("project", project_name, target, **options)
 
 
         # Be a friend once again.
         # 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 = """
         nextsteps = """
 Next steps:
 Next steps:
@@ -116,11 +127,11 @@ Next steps:
     4. python manage.py runserver
     4. python manage.py runserver
     5. Go to http://localhost:8000/admin/ and start editing!
     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 = {
 COMMANDS = {
-    'start': CreateProject(),
+    "start": CreateProject(),
 }
 }
 
 
 
 
@@ -129,15 +140,18 @@ def prog_name():
 
 
 
 
 def help_index():
 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()):
     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):
 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)
     sys.exit(1)
 
 
 
 
@@ -148,7 +162,7 @@ def main():
         help_index()
         help_index()
         return
         return
 
 
-    if command_name == 'help':
+    if command_name == "help":
         try:
         try:
             help_command_name = sys.argv[2]
             help_command_name = sys.argv[2]
         except IndexError:
         except IndexError:

+ 93 - 57
coderedcms/blocks/__init__.py

@@ -21,7 +21,7 @@ from .stream_form_blocks import (
     CoderedStreamFormRadioButtonsFieldBlock,
     CoderedStreamFormRadioButtonsFieldBlock,
     CoderedStreamFormStepBlock,
     CoderedStreamFormStepBlock,
     CoderedStreamFormTextFieldBlock,
     CoderedStreamFormTextFieldBlock,
-    CoderedStreamFormTimeFieldBlock
+    CoderedStreamFormTimeFieldBlock,
 )
 )
 from .html_blocks import (
 from .html_blocks import (
     ButtonBlock,
     ButtonBlock,
@@ -34,7 +34,7 @@ from .html_blocks import (
     PagePreviewBlock,
     PagePreviewBlock,
     QuoteBlock,
     QuoteBlock,
     RichTextBlock,
     RichTextBlock,
-    TableBlock
+    TableBlock,
 )
 )
 from .content_blocks import (  # noqa
 from .content_blocks import (  # noqa
     AccordionBlock,
     AccordionBlock,
@@ -47,13 +47,9 @@ from .content_blocks import (  # noqa
     NavExternalLinkWithSubLinkBlock,
     NavExternalLinkWithSubLinkBlock,
     NavPageLinkWithSubLinkBlock,
     NavPageLinkWithSubLinkBlock,
     PriceListBlock,
     PriceListBlock,
-    ReusableContentBlock
-)
-from .layout_blocks import (
-    CardGridBlock,
-    GridBlock,
-    HeroBlock
+    ReusableContentBlock,
 )
 )
+from .layout_blocks import CardGridBlock, GridBlock, HeroBlock
 from .base_blocks import (  # noqa
 from .base_blocks import (  # noqa
     BaseBlock,
     BaseBlock,
     BaseLayoutBlock,
     BaseLayoutBlock,
@@ -68,71 +64,111 @@ from .base_blocks import (  # noqa
 # Collections of blocks commonly used together.
 # Collections of blocks commonly used together.
 
 
 HTML_STREAMBLOCKS = [
 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 + [
 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 = [
 NAVIGATION_STREAMBLOCKS = [
-    ('page_link', NavPageLinkWithSubLinkBlock()),
-    ('external_link', NavExternalLinkWithSubLinkBlock()),
-    ('document_link', NavDocumentLinkWithSubLinkBlock()),
+    ("page_link", NavPageLinkWithSubLinkBlock()),
+    ("external_link", NavExternalLinkWithSubLinkBlock()),
+    ("document_link", NavDocumentLinkWithSubLinkBlock()),
 ]
 ]
 
 
 BASIC_LAYOUT_STREAMBLOCKS = [
 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 = [
 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 = [
 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 = [
 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.
     Enables choosing a ClassifierTerm in the streamfield.
     Lazy loads the target_model from the string to avoid recursive imports.
     Lazy loads the target_model from the string to avoid recursive imports.
     """
     """
+
     widget = forms.Select
     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._required = required
         self._help_text = help_text
         self._help_text = help_text
         self._label = label
         self._label = label
@@ -30,12 +33,14 @@ class ClassifierTermChooserBlock(blocks.FieldBlock):
 
 
     @cached_property
     @cached_property
     def target_model(self):
     def target_model(self):
-        return resolve_model_string('coderedcms.ClassifierTerm')
+        return resolve_model_string("coderedcms.ClassifierTerm")
 
 
     @cached_property
     @cached_property
     def field(self):
     def field(self):
         return forms.ModelChoiceField(
         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,
             widget=self.widget,
             required=self._required,
             required=self._required,
             label=self._label,
             label=self._label,
@@ -63,10 +68,13 @@ class CollectionChooserBlock(blocks.FieldBlock):
     """
     """
     Enables choosing a wagtail Collection in the streamfield.
     Enables choosing a wagtail Collection in the streamfield.
     """
     """
+
     target_model = Collection
     target_model = Collection
     widget = forms.Select
     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._required = required
         self._help_text = help_text
         self._help_text = help_text
         self._label = label
         self._label = label
@@ -75,7 +83,7 @@ class CollectionChooserBlock(blocks.FieldBlock):
     @cached_property
     @cached_property
     def field(self):
     def field(self):
         return forms.ModelChoiceField(
         return forms.ModelChoiceField(
-            queryset=self.target_model.objects.all().order_by('name'),
+            queryset=self.target_model.objects.all().order_by("name"),
             widget=self.widget,
             widget=self.widget,
             required=self._required,
             required=self._required,
             label=self._label,
             label=self._label,
@@ -103,22 +111,23 @@ class ButtonMixin(blocks.StructBlock):
     """
     """
     Standard style and size options for buttons.
     Standard style and size options for buttons.
     """
     """
+
     button_title = blocks.CharBlock(
     button_title = blocks.CharBlock(
         max_length=255,
         max_length=255,
         required=True,
         required=True,
-        label=_('Button Title'),
+        label=_("Button Title"),
     )
     )
     button_style = blocks.ChoiceBlock(
     button_style = blocks.ChoiceBlock(
         choices=crx_settings.CRX_FRONTEND_BTN_STYLE_CHOICES,
         choices=crx_settings.CRX_FRONTEND_BTN_STYLE_CHOICES,
         default=crx_settings.CRX_FRONTEND_BTN_STYLE_DEFAULT,
         default=crx_settings.CRX_FRONTEND_BTN_STYLE_DEFAULT,
         required=False,
         required=False,
-        label=_('Button Style'),
+        label=_("Button Style"),
     )
     )
     button_size = blocks.ChoiceBlock(
     button_size = blocks.ChoiceBlock(
         choices=crx_settings.CRX_FRONTEND_BTN_SIZE_CHOICES,
         choices=crx_settings.CRX_FRONTEND_BTN_SIZE_CHOICES,
         default=crx_settings.CRX_FRONTEND_BTN_SIZE_DEFAULT,
         default=crx_settings.CRX_FRONTEND_BTN_SIZE_DEFAULT,
         required=False,
         required=False,
-        label=_('Button Size'),
+        label=_("Button Size"),
     )
     )
 
 
 
 
@@ -127,23 +136,26 @@ class CoderedAdvSettings(blocks.StructBlock):
     Common fields each block should have,
     Common fields each block should have,
     which are hidden under the block's "Advanced Settings" dropdown.
     which are hidden under the block's "Advanced Settings" dropdown.
     """
     """
+
     # placeholder, real value get set in __init__()
     # placeholder, real value get set in __init__()
     custom_template = blocks.Block()
     custom_template = blocks.Block()
 
 
     custom_css_class = blocks.CharBlock(
     custom_css_class = blocks.CharBlock(
         required=False,
         required=False,
         max_length=255,
         max_length=255,
-        label=_('Custom CSS Class'),
+        label=_("Custom CSS Class"),
     )
     )
     custom_id = blocks.CharBlock(
     custom_id = blocks.CharBlock(
         required=False,
         required=False,
         max_length=255,
         max_length=255,
-        label=_('Custom ID'),
+        label=_("Custom ID"),
     )
     )
 
 
     class Meta:
     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):
     def __init__(self, local_blocks=None, template_choices=None, **kwargs):
         if not local_blocks:
         if not local_blocks:
@@ -151,12 +163,13 @@ class CoderedAdvSettings(blocks.StructBlock):
 
 
         local_blocks += (
         local_blocks += (
             (
             (
-                'custom_template',
+                "custom_template",
                 blocks.ChoiceBlock(
                 blocks.ChoiceBlock(
                     choices=template_choices,
                     choices=template_choices,
                     default=None,
                     default=None,
                     required=False,
                     required=False,
-                    label=_('Template'))
+                    label=_("Template"),
+                ),
             ),
             ),
         )
         )
 
 
@@ -167,15 +180,16 @@ class CoderedAdvTrackingSettings(CoderedAdvSettings):
     """
     """
     CoderedAdvSettings plus additional tracking fields.
     CoderedAdvSettings plus additional tracking fields.
     """
     """
+
     ga_tracking_event_category = blocks.CharBlock(
     ga_tracking_event_category = blocks.CharBlock(
         required=False,
         required=False,
         max_length=255,
         max_length=255,
-        label=_('Tracking Event Category'),
+        label=_("Tracking Event Category"),
     )
     )
     ga_tracking_event_label = blocks.CharBlock(
     ga_tracking_event_label = blocks.CharBlock(
         required=False,
         required=False,
         max_length=255,
         max_length=255,
-        label=_('Tracking Event Label'),
+        label=_("Tracking Event Label"),
     )
     )
 
 
 
 
@@ -183,12 +197,15 @@ class CoderedAdvColumnSettings(CoderedAdvSettings):
     """
     """
     BaseBlockSettings plus additional column fields.
     BaseBlockSettings plus additional column fields.
     """
     """
+
     column_breakpoint = blocks.ChoiceBlock(
     column_breakpoint = blocks.ChoiceBlock(
         choices=crx_settings.CRX_FRONTEND_COL_BREAK_CHOICES,
         choices=crx_settings.CRX_FRONTEND_COL_BREAK_CHOICES,
         default=crx_settings.CRX_FRONTEND_COL_BREAK_DEFAULT,
         default=crx_settings.CRX_FRONTEND_COL_BREAK_DEFAULT,
         required=False,
         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.
     Common attributes for all blocks used in Wagtail CRX.
     """
     """
+
     # subclasses can override this to determine the advanced settings class
     # subclasses can override this to determine the advanced settings class
     advsettings_class = CoderedAdvSettings
     advsettings_class = CoderedAdvSettings
 
 
@@ -207,20 +225,21 @@ class BaseBlock(blocks.StructBlock):
         Construct and inject settings block, then initialize normally.
         Construct and inject settings block, then initialize normally.
         """
         """
         klassname = self.__class__.__name__.lower()
         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:
         if not local_blocks:
             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)
         super().__init__(local_blocks, **kwargs)
 
 
     def render(self, value, context=None):
     def render(self, value, context=None):
-        template = value['settings']['custom_template']
+        template = value["settings"]["custom_template"]
 
 
         if not template:
         if not template:
             template = self.get_template(context=context)
             template = self.get_template(context=context)
@@ -239,6 +258,7 @@ class BaseLayoutBlock(BaseBlock):
     """
     """
     Common attributes for all blocks used in Wagtail CRX.
     Common attributes for all blocks used in Wagtail CRX.
     """
     """
+
     # Subclasses can override this to provide a default list of blocks for the content.
     # Subclasses can override this to provide a default list of blocks for the content.
     content_streamblocks = []
     content_streamblocks = []
 
 
@@ -247,7 +267,12 @@ class BaseLayoutBlock(BaseBlock):
             local_blocks = self.content_streamblocks
             local_blocks = self.content_streamblocks
 
 
         if local_blocks:
         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)
         super().__init__(local_blocks, **kwargs)
 
 
@@ -256,11 +281,12 @@ class LinkStructValue(blocks.StructValue):
     """
     """
     Generates a URL for blocks with multiple link choices.
     Generates a URL for blocks with multiple link choices.
     """
     """
+
     @property
     @property
     def url(self):
     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:
         if page and ext:
             return "{0}{1}".format(page.url, ext)
             return "{0}{1}".format(page.url, ext)
         elif page:
         elif page:
@@ -275,18 +301,19 @@ class BaseLinkBlock(BaseBlock):
     """
     """
     Common attributes for creating a link within the CMS.
     Common attributes for creating a link within the CMS.
     """
     """
+
     page_link = blocks.PageChooserBlock(
     page_link = blocks.PageChooserBlock(
         required=False,
         required=False,
-        label=_('Page link'),
+        label=_("Page link"),
     )
     )
     doc_link = DocumentChooserBlock(
     doc_link = DocumentChooserBlock(
         required=False,
         required=False,
-        label=_('Document link'),
+        label=_("Document link"),
     )
     )
     other_link = blocks.CharBlock(
     other_link = blocks.CharBlock(
         required=False,
         required=False,
         max_length=255,
         max_length=255,
-        label=_('Other link'),
+        label=_("Other link"),
     )
     )
 
 
     advsettings_class = CoderedAdvTrackingSettings
     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.images.blocks import ImageChooserBlock
 from wagtail.snippets.blocks import SnippetChooserBlock
 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
 from .html_blocks import ButtonBlock
 
 
 
 
@@ -17,60 +22,62 @@ class AccordionBlock(BaseBlock):
     Allows selecting an accordion snippet
     Allows selecting an accordion snippet
     """
     """
 
 
-    accordion = SnippetChooserBlock('coderedcms.Accordion')
+    accordion = SnippetChooserBlock("coderedcms.Accordion")
 
 
     class Meta:
     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):
 class CardBlock(BaseBlock):
     """
     """
     A component of information with image, text, and buttons.
     A component of information with image, text, and buttons.
     """
     """
+
     image = ImageChooserBlock(
     image = ImageChooserBlock(
         required=False,
         required=False,
         max_length=255,
         max_length=255,
-        label=_('Image'),
+        label=_("Image"),
     )
     )
     title = blocks.CharBlock(
     title = blocks.CharBlock(
         required=False,
         required=False,
         max_length=255,
         max_length=255,
-        label=_('Title'),
+        label=_("Title"),
     )
     )
     subtitle = blocks.CharBlock(
     subtitle = blocks.CharBlock(
         required=False,
         required=False,
         max_length=255,
         max_length=255,
-        label=_('Subtitle'),
+        label=_("Subtitle"),
     )
     )
     description = blocks.RichTextBlock(
     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 = blocks.StreamBlock(
-        [('Links', ButtonBlock())],
+        [("Links", ButtonBlock())],
         blank=True,
         blank=True,
         required=False,
         required=False,
-        label=_('Links'),
+        label=_("Links"),
     )
     )
 
 
     class Meta:
     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):
 class CarouselBlock(BaseBlock):
     """
     """
     Enables choosing a Carousel snippet.
     Enables choosing a Carousel snippet.
     """
     """
-    carousel = SnippetChooserBlock('coderedcms.Carousel')
+
+    carousel = SnippetChooserBlock("coderedcms.Carousel")
 
 
     class Meta:
     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):
 class ImageGalleryBlock(BaseBlock):
@@ -78,54 +85,61 @@ class ImageGalleryBlock(BaseBlock):
     Show a collection of images with interactive previews that expand to
     Show a collection of images with interactive previews that expand to
     full size images in a modal.
     full size images in a modal.
     """
     """
+
     collection = CollectionChooserBlock(
     collection = CollectionChooserBlock(
         required=True,
         required=True,
-        label=_('Image Collection'),
+        label=_("Image Collection"),
     )
     )
 
 
     class Meta:
     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):
 class ModalBlock(ButtonMixin, BaseLayoutBlock):
     """
     """
     Renders a button that then opens a popup/modal with content.
     Renders a button that then opens a popup/modal with content.
     """
     """
+
     header = blocks.CharBlock(
     header = blocks.CharBlock(
         required=False,
         required=False,
         max_length=255,
         max_length=255,
-        label=_('Modal heading'),
+        label=_("Modal heading"),
     )
     )
     content = blocks.StreamBlock(
     content = blocks.StreamBlock(
         [],
         [],
-        label=_('Modal content'),
+        label=_("Modal content"),
     )
     )
     footer = blocks.StreamBlock(
     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,
         required=False,
-        label=_('Modal footer'),
+        label=_("Modal footer"),
     )
     )
 
 
     class Meta:
     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):
 class NavBaseLinkBlock(BaseBlock):
     display_text = blocks.CharBlock(
     display_text = blocks.CharBlock(
         required=False,
         required=False,
         max_length=255,
         max_length=255,
-        label=_('Display text'),
+        label=_("Display text"),
     )
     )
     image = ImageChooserBlock(
     image = ImageChooserBlock(
         required=False,
         required=False,
-        label=_('Image'),
+        label=_("Image"),
     )
     )
 
 
 
 
@@ -133,54 +147,58 @@ class NavExternalLinkBlock(NavBaseLinkBlock):
     """
     """
     External link.
     External link.
     """
     """
+
     link = blocks.CharBlock(
     link = blocks.CharBlock(
         required=False,
         required=False,
-        label=_('URL'),
+        label=_("URL"),
     )
     )
 
 
     class Meta:
     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):
 class NavPageLinkBlock(NavBaseLinkBlock):
     """
     """
     Page link.
     Page link.
     """
     """
+
     page = blocks.PageChooserBlock(
     page = blocks.PageChooserBlock(
-        label=_('Page'),
+        label=_("Page"),
     )
     )
 
 
     class Meta:
     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):
 class NavDocumentLinkBlock(NavBaseLinkBlock):
     """
     """
     Document link.
     Document link.
     """
     """
+
     document = DocumentChooserBlock(
     document = DocumentChooserBlock(
-        label=_('Document'),
+        label=_("Document"),
     )
     )
 
 
     class Meta:
     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):
 class NavSubLinkBlock(BaseBlock):
     """
     """
     Streamblock for rendering nested sub-links.
     Streamblock for rendering nested sub-links.
     """
     """
+
     sub_links = blocks.StreamBlock(
     sub_links = blocks.StreamBlock(
         [
         [
-            ('page_link', NavPageLinkBlock()),
-            ('external_link', NavExternalLinkBlock()),
-            ('document_link', NavDocumentLinkBlock()),
+            ("page_link", NavPageLinkBlock()),
+            ("external_link", NavExternalLinkBlock()),
+            ("document_link", NavDocumentLinkBlock()),
         ],
         ],
         required=False,
         required=False,
-        label=_('Sub-links'),
+        label=_("Sub-links"),
     )
     )
 
 
 
 
@@ -188,111 +206,122 @@ class NavExternalLinkWithSubLinkBlock(NavSubLinkBlock, NavExternalLinkBlock):
     """
     """
     Extermal link with option for sub-links.
     Extermal link with option for sub-links.
     """
     """
+
     class Meta:
     class Meta:
-        label = _('External link with sub-links')
+        label = _("External link with sub-links")
 
 
 
 
 class NavPageLinkWithSubLinkBlock(NavSubLinkBlock, NavPageLinkBlock):
 class NavPageLinkWithSubLinkBlock(NavSubLinkBlock, NavPageLinkBlock):
     """
     """
     Page link with option for sub-links or showing child pages.
     Page link with option for sub-links or showing child pages.
     """
     """
+
     show_child_links = blocks.BooleanBlock(
     show_child_links = blocks.BooleanBlock(
         required=False,
         required=False,
         default=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:
     class Meta:
-        label = _('Page link with sub-links')
+        label = _("Page link with sub-links")
 
 
 
 
 class NavDocumentLinkWithSubLinkBlock(NavSubLinkBlock, NavDocumentLinkBlock):
 class NavDocumentLinkWithSubLinkBlock(NavSubLinkBlock, NavDocumentLinkBlock):
     """
     """
     Document link with option for sub-links.
     Document link with option for sub-links.
     """
     """
+
     class Meta:
     class Meta:
-        label = _('Document link with sub-links')
+        label = _("Document link with sub-links")
 
 
 
 
 class PriceListItemBlock(BaseBlock):
 class PriceListItemBlock(BaseBlock):
     """
     """
     Represents one item in a PriceListBlock, such as an entree in a restaurant menu.
     Represents one item in a PriceListBlock, such as an entree in a restaurant menu.
     """
     """
+
     image = ImageChooserBlock(
     image = ImageChooserBlock(
         required=False,
         required=False,
-        label=_('Image'),
+        label=_("Image"),
     )
     )
     name = blocks.CharBlock(
     name = blocks.CharBlock(
         required=True,
         required=True,
         max_length=255,
         max_length=255,
-        label=_('Name'),
+        label=_("Name"),
     )
     )
     description = blocks.TextBlock(
     description = blocks.TextBlock(
         required=False,
         required=False,
         rows=4,
         rows=4,
-        label=_('Description'),
+        label=_("Description"),
     )
     )
     price = blocks.CharBlock(
     price = blocks.CharBlock(
         required=True,
         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:
     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):
 class PriceListBlock(BaseBlock):
     """
     """
     A price list, such as a menu for a restaurant.
     A price list, such as a menu for a restaurant.
     """
     """
+
     heading = blocks.CharBlock(
     heading = blocks.CharBlock(
         required=False,
         required=False,
         max_length=255,
         max_length=255,
-        label=_('Heading'),
+        label=_("Heading"),
     )
     )
     items = blocks.StreamBlock(
     items = blocks.StreamBlock(
         [
         [
-            ('item', PriceListItemBlock()),
+            ("item", PriceListItemBlock()),
         ],
         ],
-        label=_('Items'),
+        label=_("Items"),
     )
     )
 
 
     class Meta:
     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):
 class ContentWallBlock(BaseBlock):
     """
     """
     Enables choosing a ContentWall snippet.
     Enables choosing a ContentWall snippet.
     """
     """
-    content_wall = SnippetChooserBlock('coderedcms.ContentWall')
+
+    content_wall = SnippetChooserBlock("coderedcms.ContentWall")
     show_content_wall_on_children = blocks.BooleanBlock(
     show_content_wall_on_children = blocks.BooleanBlock(
         required=False,
         required=False,
         default=False,
         default=False,
-        verbose_name=_('Show content walls on children pages?'),
+        verbose_name=_("Show content walls on children pages?"),
         help_text=_(
         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:
     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):
 class ReusableContentBlock(BaseBlock):
     """
     """
     Enables choosing a ResusableContent snippet.
     Enables choosing a ResusableContent snippet.
     """
     """
-    content = SnippetChooserBlock('coderedcms.ReusableContent')
+
+    content = SnippetChooserBlock("coderedcms.ReusableContent")
 
 
     class Meta:
     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):
 class ButtonBlock(ButtonMixin, BaseLinkBlock):
     """
     """
     A link styled as a button.
     A link styled as a button.
     """
     """
+
     class Meta:
     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
         value_class = LinkStructValue
 
 
 
 
@@ -42,157 +43,170 @@ class DownloadBlock(ButtonMixin, BaseBlock):
     """
     """
     Link to a file that can be downloaded.
     Link to a file that can be downloaded.
     """
     """
+
     downloadable_file = DocumentChooserBlock(
     downloadable_file = DocumentChooserBlock(
         required=False,
         required=False,
-        label=_('Document link'),
+        label=_("Document link"),
     )
     )
 
 
     advsettings_class = CoderedAdvTrackingSettings
     advsettings_class = CoderedAdvTrackingSettings
 
 
     class Meta:
     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):
 class EmbedGoogleMapBlock(BaseBlock):
     """
     """
     An embedded Google map in an <iframe>.
     An embedded Google map in an <iframe>.
     """
     """
+
     search = blocks.CharBlock(
     search = blocks.CharBlock(
         required=False,
         required=False,
         max_length=255,
         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(
     map_title = blocks.CharBlock(
         required=False,
         required=False,
         max_length=255,
         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(
     place_id = blocks.CharBlock(
         required=False,
         required=False,
         max_length=255,
         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(
     map_zoom_level = blocks.IntegerBlock(
         required=False,
         required=False,
         default=14,
         default=14,
-        label=_('Map zoom level'),
+        label=_("Map zoom level"),
         help_text=_(
         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:
     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):
 class EmbedVideoBlock(BaseBlock):
     """
     """
     Embedded media using stock wagtail functionality.
     Embedded media using stock wagtail functionality.
     """
     """
+
     url = EmbedBlock(
     url = EmbedBlock(
         required=True,
         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:
     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):
 class H1Block(BaseBlock):
     """
     """
     An <h1> heading.
     An <h1> heading.
     """
     """
+
     text = blocks.CharBlock(
     text = blocks.CharBlock(
         max_length=255,
         max_length=255,
-        label=_('Text'),
+        label=_("Text"),
     )
     )
 
 
     class Meta:
     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):
 class H2Block(BaseBlock):
     """
     """
     An <h2> heading.
     An <h2> heading.
     """
     """
+
     text = blocks.CharBlock(
     text = blocks.CharBlock(
         max_length=255,
         max_length=255,
-        label=_('Text'),
+        label=_("Text"),
     )
     )
 
 
     class Meta:
     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):
 class H3Block(BaseBlock):
     """
     """
     An <h3> heading.
     An <h3> heading.
     """
     """
+
     text = blocks.CharBlock(
     text = blocks.CharBlock(
         max_length=255,
         max_length=255,
-        label=_('Text'),
+        label=_("Text"),
     )
     )
 
 
     class Meta:
     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):
 class TableBlock(BaseBlock):
     table = WagtailTableBlock()
     table = WagtailTableBlock()
 
 
     class Meta:
     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):
 class ImageBlock(BaseBlock):
     """
     """
     An <img>, by default styled responsively to fill its container.
     An <img>, by default styled responsively to fill its container.
     """
     """
+
     image = ImageChooserBlock(
     image = ImageChooserBlock(
-        label=_('Image'),
+        label=_("Image"),
     )
     )
 
 
     class Meta:
     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):
 class ImageLinkBlock(BaseLinkBlock):
     """
     """
     An <a> with an image inside it, instead of text.
     An <a> with an image inside it, instead of text.
     """
     """
+
     image = ImageChooserBlock(
     image = ImageChooserBlock(
-        label=_('Image'),
+        label=_("Image"),
     )
     )
     alt_text = blocks.CharBlock(
     alt_text = blocks.CharBlock(
         max_length=255,
         max_length=255,
         required=True,
         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:
     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
         value_class = LinkStructValue
 
 
 
 
@@ -200,55 +214,66 @@ class PageListBlock(BaseBlock):
     """
     """
     Renders a preview of selected pages.
     Renders a preview of selected pages.
     """
     """
+
     indexed_by = blocks.PageChooserBlock(
     indexed_by = blocks.PageChooserBlock(
         required=True,
         required=True,
-        label=_('Parent page'),
+        label=_("Parent page"),
         help_text=_(
         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(
     classified_by = ClassifierTermChooserBlock(
         required=False,
         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(
     show_preview = blocks.BooleanBlock(
         required=False,
         required=False,
         default=False,
         default=False,
-        label=_('Show body preview'),
+        label=_("Show body preview"),
     )
     )
     num_posts = blocks.IntegerBlock(
     num_posts = blocks.IntegerBlock(
         default=3,
         default=3,
-        label=_('Number of pages to show'),
+        label=_("Number of pages to show"),
     )
     )
 
 
     class Meta:
     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):
     def get_context(self, value, parent_context=None):
 
 
         context = super().get_context(value, parent_context=parent_context)
         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()`,
         # try to use the CoderedPage `get_index_children()`,
         # but fall back to get_children if this is a non-CoderedPage
         # 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()
             pages = indexer.get_index_children()
-            if value['classified_by']:
+            if value["classified_by"]:
                 try:
                 try:
-                    pages = pages.filter(classifier_terms=value['classified_by'])
+                    pages = pages.filter(
+                        classifier_terms=value["classified_by"]
+                    )
                 except AttributeError:
                 except AttributeError:
                     # `pages` is not a queryset, or is not a queryset of CoderedPage.
                     # `pages` is not a queryset, or is not a queryset of CoderedPage.
                     logger.warning(
                     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:
         else:
             pages = indexer.get_children().live()
             pages = indexer.get_children().live()
 
 
-        context['pages'] = pages[:value['num_posts']]
+        context["pages"] = pages[: value["num_posts"]]
         return context
         return context
 
 
 
 
@@ -256,39 +281,41 @@ class PagePreviewBlock(BaseBlock):
     """
     """
     Renders a preview of a specific page.
     Renders a preview of a specific page.
     """
     """
+
     page = blocks.PageChooserBlock(
     page = blocks.PageChooserBlock(
         required=True,
         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:
     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):
 class QuoteBlock(BaseBlock):
     """
     """
     A <blockquote>.
     A <blockquote>.
     """
     """
+
     text = blocks.TextBlock(
     text = blocks.TextBlock(
         required=True,
         required=True,
         rows=4,
         rows=4,
-        label=_('Quote Text'),
+        label=_("Quote Text"),
     )
     )
     author = blocks.CharBlock(
     author = blocks.CharBlock(
         required=False,
         required=False,
         max_length=255,
         max_length=255,
-        label=_('Author'),
+        label=_("Author"),
     )
     )
 
 
     class Meta:
     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 RichTextBlock(blocks.RichTextBlock):
     class Meta:
     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.
     Renders content in a column.
     """
     """
+
     column_size = blocks.ChoiceBlock(
     column_size = blocks.ChoiceBlock(
         choices=crx_settings.CRX_FRONTEND_COL_SIZE_CHOICES,
         choices=crx_settings.CRX_FRONTEND_COL_SIZE_CHOICES,
         default=crx_settings.CRX_FRONTEND_COL_SIZE_DEFAULT,
         default=crx_settings.CRX_FRONTEND_COL_SIZE_DEFAULT,
         required=False,
         required=False,
-        label=_('Column size'),
+        label=_("Column size"),
     )
     )
 
 
     advsettings_class = CoderedAdvColumnSettings
     advsettings_class = CoderedAdvColumnSettings
 
 
     class Meta:
     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):
 class GridBlock(BaseLayoutBlock):
     """
     """
     Renders a row of columns.
     Renders a row of columns.
     """
     """
+
     fluid = blocks.BooleanBlock(
     fluid = blocks.BooleanBlock(
         required=False,
         required=False,
-        label=_('Full width'),
+        label=_("Full width"),
     )
     )
 
 
     class Meta:
     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):
     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):
 class CardGridBlock(BaseLayoutBlock):
     """
     """
     Renders a row of cards.
     Renders a row of cards.
     """
     """
+
     fluid = blocks.BooleanBlock(
     fluid = blocks.BooleanBlock(
         required=False,
         required=False,
-        label=_('Full width'),
+        label=_("Full width"),
     )
     )
 
 
     class Meta:
     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):
 class HeroBlock(BaseLayoutBlock):
@@ -79,34 +78,35 @@ class HeroBlock(BaseLayoutBlock):
     fluid = blocks.BooleanBlock(
     fluid = blocks.BooleanBlock(
         required=False,
         required=False,
         default=True,
         default=True,
-        label=_('Full width'),
+        label=_("Full width"),
     )
     )
     is_parallax = blocks.BooleanBlock(
     is_parallax = blocks.BooleanBlock(
         required=False,
         required=False,
-        label=_('Parallax Effect'),
+        label=_("Parallax Effect"),
         help_text=_(
         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)
     background_image = ImageChooserBlock(required=False)
     tile_image = blocks.BooleanBlock(
     tile_image = blocks.BooleanBlock(
         required=False,
         required=False,
         default=False,
         default=False,
-        label=_('Tile background image'),
+        label=_("Tile background image"),
     )
     )
     background_color = blocks.CharBlock(
     background_color = blocks.CharBlock(
         required=False,
         required=False,
         max_length=255,
         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(
     foreground_color = blocks.CharBlock(
         required=False,
         required=False,
         max_length=255,
         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:
     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.wagtail_flexible_forms import blocks as form_blocks
 from coderedcms.blocks.base_blocks import BaseBlock, CoderedAdvSettings
 from coderedcms.blocks.base_blocks import BaseBlock, CoderedAdvSettings
 from coderedcms.forms import (
 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(
     condition_trigger_id = blocks.CharBlock(
         required=False,
         required=False,
         max_length=255,
         max_length=255,
-        label=_('Condition Trigger ID'),
+        label=_("Condition Trigger ID"),
         help_text=_(
         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(
     condition_trigger_value = blocks.CharBlock(
         required=False,
         required=False,
         max_length=255,
         max_length=255,
-        label=_('Condition Trigger Value'),
+        label=_("Condition Trigger Value"),
         help_text=_(
         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
     advsettings_class = CoderedFormAdvSettings
 
 
 
 
-class CoderedStreamFormFieldBlock(form_blocks.OptionalFormFieldBlock, FormBlockMixin):
+class CoderedStreamFormFieldBlock(
+    form_blocks.OptionalFormFieldBlock, FormBlockMixin
+):
     pass
     pass
 
 
 
 
-class CoderedStreamFormCharFieldBlock(form_blocks.CharFieldBlock, FormBlockMixin):
+class CoderedStreamFormCharFieldBlock(
+    form_blocks.CharFieldBlock, FormBlockMixin
+):
     class Meta:
     class Meta:
         label = _("Text or Email input")
         label = _("Text or Email input")
         icon = "cr-window-minimize"
         icon = "cr-window-minimize"
 
 
 
 
-class CoderedStreamFormTextFieldBlock(form_blocks.TextFieldBlock, FormBlockMixin):
+class CoderedStreamFormTextFieldBlock(
+    form_blocks.TextFieldBlock, FormBlockMixin
+):
     class Meta:
     class Meta:
         label = _("Multi-line text")
         label = _("Multi-line text")
         icon = "cr-align-left"
         icon = "cr-align-left"
 
 
 
 
-class CoderedStreamFormNumberFieldBlock(form_blocks.NumberFieldBlock, FormBlockMixin):
+class CoderedStreamFormNumberFieldBlock(
+    form_blocks.NumberFieldBlock, FormBlockMixin
+):
     class Meta:
     class Meta:
         label = _("Numbers only")
         label = _("Numbers only")
         icon = "cr-hashtag"
         icon = "cr-hashtag"
 
 
 
 
-class CoderedStreamFormCheckboxFieldBlock(form_blocks.CheckboxFieldBlock, FormBlockMixin):
+class CoderedStreamFormCheckboxFieldBlock(
+    form_blocks.CheckboxFieldBlock, FormBlockMixin
+):
     class Meta:
     class Meta:
         label = _("Single Checkbox")
         label = _("Single Checkbox")
         icon = "cr-check-square-o"
         icon = "cr-check-square-o"
 
 
 
 
-class CoderedStreamFormRadioButtonsFieldBlock(form_blocks.RadioButtonsFieldBlock, FormBlockMixin):
+class CoderedStreamFormRadioButtonsFieldBlock(
+    form_blocks.RadioButtonsFieldBlock, FormBlockMixin
+):
     class Meta:
     class Meta:
         label = _("Radios")
         label = _("Radios")
         icon = "list-ul"
         icon = "list-ul"
 
 
 
 
-class CoderedStreamFormDropdownFieldBlock(form_blocks.DropdownFieldBlock, FormBlockMixin):
+class CoderedStreamFormDropdownFieldBlock(
+    form_blocks.DropdownFieldBlock, FormBlockMixin
+):
     class Meta:
     class Meta:
         label = _("Dropdown")
         label = _("Dropdown")
         icon = "cr-list-alt"
         icon = "cr-list-alt"
 
 
 
 
-class CoderedStreamFormCheckboxesFieldBlock(form_blocks.CheckboxesFieldBlock, FormBlockMixin):
+class CoderedStreamFormCheckboxesFieldBlock(
+    form_blocks.CheckboxesFieldBlock, FormBlockMixin
+):
     class Meta:
     class Meta:
         label = _("Checkboxes")
         label = _("Checkboxes")
         icon = "list-ul"
         icon = "list-ul"
 
 
 
 
-class CoderedStreamFormDateFieldBlock(form_blocks.DateFieldBlock, FormBlockMixin):
+class CoderedStreamFormDateFieldBlock(
+    form_blocks.DateFieldBlock, FormBlockMixin
+):
     class Meta:
     class Meta:
         label = _("Date")
         label = _("Date")
         icon = "date"
         icon = "date"
@@ -91,7 +116,9 @@ class CoderedStreamFormDateFieldBlock(form_blocks.DateFieldBlock, FormBlockMixin
     widget = CoderedDateInput
     widget = CoderedDateInput
 
 
 
 
-class CoderedStreamFormTimeFieldBlock(form_blocks.TimeFieldBlock, FormBlockMixin):
+class CoderedStreamFormTimeFieldBlock(
+    form_blocks.TimeFieldBlock, FormBlockMixin
+):
     class Meta:
     class Meta:
         label = _("Time")
         label = _("Time")
         icon = "time"
         icon = "time"
@@ -100,7 +127,9 @@ class CoderedStreamFormTimeFieldBlock(form_blocks.TimeFieldBlock, FormBlockMixin
     widget = CoderedTimeInput
     widget = CoderedTimeInput
 
 
 
 
-class CoderedStreamFormDateTimeFieldBlock(form_blocks.DateTimeFieldBlock, FormBlockMixin):
+class CoderedStreamFormDateTimeFieldBlock(
+    form_blocks.DateTimeFieldBlock, FormBlockMixin
+):
     class Meta:
     class Meta:
         label = _("Date and Time")
         label = _("Date and Time")
         icon = "date"
         icon = "date"
@@ -109,13 +138,17 @@ class CoderedStreamFormDateTimeFieldBlock(form_blocks.DateTimeFieldBlock, FormBl
     widget = CoderedDateTimeInput
     widget = CoderedDateTimeInput
 
 
 
 
-class CoderedStreamFormImageFieldBlock(form_blocks.ImageFieldBlock, FormBlockMixin):
+class CoderedStreamFormImageFieldBlock(
+    form_blocks.ImageFieldBlock, FormBlockMixin
+):
     class Meta:
     class Meta:
         label = _("Image Upload")
         label = _("Image Upload")
         icon = "image"
         icon = "image"
 
 
 
 
-class CoderedStreamFormFileFieldBlock(form_blocks.FileFieldBlock, FormBlockMixin):
+class CoderedStreamFormFileFieldBlock(
+    form_blocks.FileFieldBlock, FormBlockMixin
+):
     class Meta:
     class Meta:
         label = _("Secure File Upload")
         label = _("Secure File Upload")
         icon = "upload"
         icon = "upload"
@@ -128,7 +161,5 @@ class CoderedStreamFormStepBlock(form_blocks.FormStepBlock):
 
 
     def __init__(self, local_blocks=None, **kwargs):
     def __init__(self, local_blocks=None, **kwargs):
         super().__init__(
         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:
     Inspired by:
     https://cynthiakiser.com/blog/2022/01/06/trimming-wagtail-migration-cruft.html
     https://cynthiakiser.com/blog/2022/01/06/trimming-wagtail-migration-cruft.html
     """
     """
+
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
         """
         """
         Patch init to work around django reconstruct not sending empty args.
         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.
     A CharField which uses the HTML5 color picker widget.
     """
     """
+
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
-        kwargs['max_length'] = 255
+        kwargs["max_length"] = 255
         super().__init__(*args, **kwargs)
         super().__init__(*args, **kwargs)
 
 
     def formfield(self, **kwargs):
     def formfield(self, **kwargs):
-        kwargs['widget'] = ColorPickerWidget
+        kwargs["widget"] = ColorPickerWidget
         return super().formfield(**kwargs)
         return super().formfield(**kwargs)
 
 
 
 
@@ -77,10 +79,13 @@ class MonospaceField(models.TextField):
     """
     """
     A TextField which renders as a large HTML textarea with monospace font.
     A TextField which renders as a large HTML textarea with monospace font.
     """
     """
+
     def formfield(self, **kwargs):
     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)
         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.db import models
 from django.http import HttpResponse
 from django.http import HttpResponse
 from django.utils.translation import gettext_lazy as _
 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.forms import FormBuilder
 from wagtail.contrib.forms.models import AbstractFormField
 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
 from coderedcms.utils import attempt_protected_media_value_conversion
 
 
 FORM_FIELD_CHOICES = (
 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
 # Files
 
 
+
 class SecureFileField(forms.FileField):
 class SecureFileField(forms.FileField):
     custom_error_messages = {
     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):
     def __init__(self, **kwargs):
@@ -65,19 +79,26 @@ class SecureFileField(forms.FileField):
 
 
     def _check_whitelist(self, value):
     def _check_whitelist(self, value):
         if crx_settings.CRX_PROTECTED_MEDIA_UPLOAD_WHITELIST:
         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):
     def _check_blacklist(self, value):
         if crx_settings.CRX_PROTECTED_MEDIA_UPLOAD_BLACKLIST:
         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
 # Date
 
 
+
 class CoderedDateInput(forms.DateInput):
 class CoderedDateInput(forms.DateInput):
-    template_name = 'coderedcms/formfields/date.html'
+    template_name = "coderedcms/formfields/date.html"
 
 
 
 
 class CoderedDateField(forms.DateField):
 class CoderedDateField(forms.DateField):
@@ -86,24 +107,31 @@ class CoderedDateField(forms.DateField):
 
 
 # Datetime
 # Datetime
 
 
+
 class CoderedDateTimeInput(forms.DateTimeInput):
 class CoderedDateTimeInput(forms.DateTimeInput):
-    template_name = 'coderedcms/formfields/datetime.html'
+    template_name = "coderedcms/formfields/datetime.html"
 
 
 
 
 class CoderedDateTimeField(forms.DateTimeField):
 class CoderedDateTimeField(forms.DateTimeField):
     widget = CoderedDateTimeInput()
     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
 # Time
 
 
+
 class CoderedTimeInput(forms.TimeInput):
 class CoderedTimeInput(forms.TimeInput):
-    template_name = 'coderedcms/formfields/time.html'
+    template_name = "coderedcms/formfields/time.html"
 
 
 
 
 class CoderedTimeField(forms.TimeField):
 class CoderedTimeField(forms.TimeField):
     widget = CoderedTimeInput()
     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):
 class CoderedFormBuilder(FormBuilder):
@@ -127,15 +155,19 @@ class CoderedFormBuilder(FormBuilder):
 class CoderedSubmissionsListView(WagtailSubmissionsListView):
 class CoderedSubmissionsListView(WagtailSubmissionsListView):
     def get_csv_response(self, context):
     def get_csv_response(self, context):
         filename = self.get_csv_filename()
         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 = 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 = []
             modified_data_row = []
             for cell in 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)
                 modified_data_row.append(modified_cell)
 
 
             writer.writerow(modified_data_row)
             writer.writerow(modified_data_row)
@@ -147,11 +179,11 @@ class CoderedFormField(AbstractFormField):
         abstract = True
         abstract = True
 
 
     field_type = models.CharField(
     field_type = models.CharField(
-        verbose_name=_('field type'),
+        verbose_name=_("field type"),
         max_length=16,
         max_length=16,
         choices=FORM_FIELD_CHOICES,
         choices=FORM_FIELD_CHOICES,
         blank=False,
         blank=False,
-        default='Single line text'
+        default="Single line text",
     )
     )
 
 
 
 
@@ -159,13 +191,13 @@ class SearchForm(forms.Form):
     s = forms.CharField(
     s = forms.CharField(
         max_length=255,
         max_length=255,
         required=False,
         required=False,
-        label=_('Search'),
+        label=_("Search"),
     )
     )
     t = forms.CharField(
     t = forms.CharField(
         widget=forms.HiddenInput,
         widget=forms.HiddenInput,
         max_length=255,
         max_length=255,
         required=False,
         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
     https://github.com/torchbox/wagtail-import-export/blob/master/wagtailimportexport/forms.py#L29
     with addition of ``page_type``.
     with addition of ``page_type``.
     """
     """
+
     page_type = forms.ChoiceField(choices=get_page_model_choices)
     page_type = forms.ChoiceField(choices=get_page_model_choices)
 
 
     file = forms.FileField(label=_("File to import"))
     file = forms.FileField(label=_("File to import"))
@@ -36,7 +37,7 @@ class ImportPagesFromCSVFileForm(forms.Form):
         queryset=Page.objects.all(),
         queryset=Page.objects.all(),
         widget=AdminPageChooser(can_choose_root=True, show_edit_link=False),
         widget=AdminPageChooser(can_choose_root=True, show_edit_link=False),
         label=_("Destination parent page"),
         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
     https://github.com/torchbox/wagtail-import-export/blob/master/wagtailimportexport/importing.py#L67
     """
     """
     for field in model._meta.get_fields():
     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)
             linked_page_id = getattr(model, field.attname)
             try:
             try:
                 # see if the linked page is one of the ones we're importing
                 # 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.
     # text / streamfields.
     page_content_type = ContentType.objects.get_for_model(Page)
     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
         # build a base Page instance from the exported content
         # (so that we pick up its title and other core attributes)
         # (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
         # clear id and treebeard-related fields so that
         # they get reassigned when we save via add_child
         # 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)
         parent_page.add_child(instance=page)
 
 
         # Custom Code to add the new pk back into the original page record.
         # 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
         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
         # 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
         # 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
         # between the source and destination sites but the page model needs
         # to exist on both.
         # to exist on both.
         # Raises LookupError exception if there is no matching model
         # 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(
         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]
         base_page = pages_by_original_id[specific_page.id]
         specific_page.base_page_ptr = base_page
         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)
         update_page_references(specific_page, pages_by_original_id)
         specific_page.save()
         specific_page.save()
 
 
-    return len(import_data['pages'])
+    return len(import_data["pages"])
 
 
 
 
 def convert_csv_to_json(csv_file, page_type):
 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)
     pages_csv_dict = csv.DictReader(csv_file)
     for row in pages_csv_dict:
     for row in pages_csv_dict:
         page_dict = copy.deepcopy(default_page_data)
         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
     return pages_json

+ 100 - 72
coderedcms/models/integration_models.py

@@ -13,29 +13,40 @@ import json
 
 
 
 
 class MailchimpSubscriberIntegrationWidget(Input):
 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):
     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)
         json_value = self.get_json_value(value)
         list_library = self.build_list_library()
         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
         return ctx
 
 
     def render_js(self, name, list_library, json_value):
     def render_js(self, name, list_library, json_value):
         ctx = {
         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)
         return render_to_string(self.js_template_name, ctx)
@@ -44,30 +55,30 @@ class MailchimpSubscriberIntegrationWidget(Input):
         if value:
         if value:
             json_value = json.loads(value)
             json_value = json.loads(value)
         else:
         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
         return json_value
 
 
     def get_stored_mailchimp_list(self, 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):
     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({})
         return json.dumps({})
 
 
     def get_selectable_mailchimp_lists(self, library):
     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():
         for k, v in library.items():
-            selectable_lists.append((k, v['name']))
+            selectable_lists.append((k, v["name"]))
 
 
         return selectable_lists
         return selectable_lists
 
 
@@ -76,23 +87,34 @@ class MailchimpSubscriberIntegrationWidget(Input):
         list_library = {}
         list_library = {}
         if mailchimp.is_active:
         if mailchimp.is_active:
             lists = mailchimp.get_lists()
             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
         return list_library
 
 
@@ -101,62 +123,68 @@ class MailchimpSubscriberIntegration(models.Model):
     class Meta:
     class Meta:
         abstract = True
         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):
     def integration_operation(self, instance, **kwargs):
         mailchimp = MailchimpApi()
         mailchimp = MailchimpApi()
         if mailchimp.is_active:
         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)
             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):
     def get_data(self):
         return json.loads(self.subscriber_json_data)
         return json.loads(self.subscriber_json_data)
 
 
     def get_merge_fields(self):
     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 {}
         return {}
 
 
     def get_list_id(self):
     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):
     def combine_interest_categories(self):
         interest_dict = {}
         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
         return interest_dict
 
 
     def render_dictionary(self, form_submission):
     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
         return rendered_dictionary
 
 
     panels = [
     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):
 def run_mailchimp_subscriber_integrations(instance, **kwargs):
-    if hasattr(instance, 'integration_panels'):
+    if hasattr(instance, "integration_panels"):
         for panel in instance.integration_panels:
         for panel in instance.integration_panels:
             for integration in getattr(instance, panel.relation_name).all():
             for integration in getattr(instance, panel.relation_name).all():
                 integration.integration_operation(instance, **kwargs)
                 integration.integration_operation(instance, **kwargs)

File diff suppressed because it is too large
+ 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.snippets.models import register_snippet
 from wagtail.images import get_image_model_string
 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.fields import CoderedStreamField
-from coderedcms.settings import crx_settings
 
 
 
 
 @register_snippet
 @register_snippet
@@ -28,37 +31,42 @@ class Carousel(ClusterableModel):
     Selected through Page StreamField bodies by the CarouselSnippetChooser in
     Selected through Page StreamField bodies by the CarouselSnippetChooser in
     snippet_choosers.py
     snippet_choosers.py
     """
     """
+
     class Meta:
     class Meta:
-        verbose_name = _('Carousel')
+        verbose_name = _("Carousel")
 
 
     name = models.CharField(
     name = models.CharField(
         max_length=255,
         max_length=255,
-        verbose_name=_('Name'),
+        verbose_name=_("Name"),
     )
     )
     show_controls = models.BooleanField(
     show_controls = models.BooleanField(
         default=True,
         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(
     show_indicators = models.BooleanField(
         default=True,
         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):
     def __str__(self):
         return self.name
         return self.name
@@ -69,37 +77,38 @@ class CarouselSlide(Orderable, models.Model):
     Represents a slide for the Carousel model. Can be modified through the
     Represents a slide for the Carousel model. Can be modified through the
     snippets UI.
     snippets UI.
     """
     """
+
     class Meta(Orderable.Meta):
     class Meta(Orderable.Meta):
-        verbose_name = _('Carousel Slide')
+        verbose_name = _("Carousel Slide")
 
 
     carousel = ParentalKey(
     carousel = ParentalKey(
         Carousel,
         Carousel,
-        related_name='carousel_slides',
-        verbose_name=_('Carousel'),
+        related_name="carousel_slides",
+        verbose_name=_("Carousel"),
     )
     )
     image = models.ForeignKey(
     image = models.ForeignKey(
         get_image_model_string(),
         get_image_model_string(),
         null=True,
         null=True,
         blank=True,
         blank=True,
         on_delete=models.SET_NULL,
         on_delete=models.SET_NULL,
-        related_name='+',
-        verbose_name=_('Image'),
+        related_name="+",
+        verbose_name=_("Image"),
     )
     )
     background_color = models.CharField(
     background_color = models.CharField(
         max_length=255,
         max_length=255,
         blank=True,
         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(
     custom_css_class = models.CharField(
         max_length=255,
         max_length=255,
         blank=True,
         blank=True,
-        verbose_name=_('Custom CSS class'),
+        verbose_name=_("Custom CSS class"),
     )
     )
     custom_id = models.CharField(
     custom_id = models.CharField(
         max_length=255,
         max_length=255,
         blank=True,
         blank=True,
-        verbose_name=_('Custom ID'),
+        verbose_name=_("Custom ID"),
     )
     )
 
 
     content = CoderedStreamField(
     content = CoderedStreamField(
@@ -108,15 +117,13 @@ class CarouselSlide(Orderable, models.Model):
         use_json_field=True,
         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
 @register_snippet
@@ -124,24 +131,25 @@ class Classifier(ClusterableModel):
     """
     """
     Simple and generic model to organize/categorize/group pages.
     Simple and generic model to organize/categorize/group pages.
     """
     """
+
     class Meta:
     class Meta:
-        verbose_name = _('Classifier')
-        verbose_name_plural = _('Classifiers')
-        ordering = ['name']
+        verbose_name = _("Classifier")
+        verbose_name_plural = _("Classifiers")
+        ordering = ["name"]
 
 
     slug = models.SlugField(
     slug = models.SlugField(
         allow_unicode=True,
         allow_unicode=True,
         unique=True,
         unique=True,
-        verbose_name=_('Slug'),
+        verbose_name=_("Slug"),
     )
     )
     name = models.CharField(
     name = models.CharField(
         max_length=255,
         max_length=255,
-        verbose_name=_('Name'),
+        verbose_name=_("Name"),
     )
     )
 
 
     panels = [
     panels = [
-        FieldPanel('name'),
-        InlinePanel('terms', label=_('Classifier Terms'))
+        FieldPanel("name"),
+        InlinePanel("terms", label=_("Classifier Terms")),
     ]
     ]
 
 
     def save(self, *args, **kwargs):
     def save(self, *args, **kwargs):
@@ -166,27 +174,28 @@ class ClassifierTerm(Orderable, models.Model):
     """
     """
     Term used to categorize a page.
     Term used to categorize a page.
     """
     """
+
     class Meta(Orderable.Meta):
     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 = ParentalKey(
         Classifier,
         Classifier,
-        related_name='terms',
-        verbose_name=_('Classifier'),
+        related_name="terms",
+        verbose_name=_("Classifier"),
     )
     )
     slug = models.SlugField(
     slug = models.SlugField(
         allow_unicode=True,
         allow_unicode=True,
         unique=True,
         unique=True,
-        verbose_name=_('Slug'),
+        verbose_name=_("Slug"),
     )
     )
     name = models.CharField(
     name = models.CharField(
         max_length=255,
         max_length=255,
-        verbose_name=_('Name'),
+        verbose_name=_("Name"),
     )
     )
 
 
     panels = [
     panels = [
-        FieldPanel('name'),
+        FieldPanel("name"),
     ]
     ]
 
 
     def save(self, *args, **kwargs):
     def save(self, *args, **kwargs):
@@ -212,40 +221,41 @@ class Navbar(models.Model):
     """
     """
     Snippet for site navigation bars (header, main menu, etc.)
     Snippet for site navigation bars (header, main menu, etc.)
     """
     """
+
     class Meta:
     class Meta:
-        verbose_name = _('Navigation Bar')
+        verbose_name = _("Navigation Bar")
 
 
     name = models.CharField(
     name = models.CharField(
         max_length=255,
         max_length=255,
-        verbose_name=_('Name'),
+        verbose_name=_("Name"),
     )
     )
     custom_css_class = models.CharField(
     custom_css_class = models.CharField(
         max_length=255,
         max_length=255,
         blank=True,
         blank=True,
-        verbose_name=_('Custom CSS Class'),
+        verbose_name=_("Custom CSS Class"),
     )
     )
     custom_id = models.CharField(
     custom_id = models.CharField(
         max_length=255,
         max_length=255,
         blank=True,
         blank=True,
-        verbose_name=_('Custom ID'),
+        verbose_name=_("Custom ID"),
     )
     )
     menu_items = CoderedStreamField(
     menu_items = CoderedStreamField(
         NAVIGATION_STREAMBLOCKS,
         NAVIGATION_STREAMBLOCKS,
-        verbose_name=_('Navigation links'),
+        verbose_name=_("Navigation links"),
         blank=True,
         blank=True,
         use_json_field=True,
         use_json_field=True,
     )
     )
 
 
     panels = [
     panels = [
-        FieldPanel('name'),
+        FieldPanel("name"),
         MultiFieldPanel(
         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):
     def __str__(self):
@@ -257,40 +267,41 @@ class Footer(models.Model):
     """
     """
     Snippet for website footer content.
     Snippet for website footer content.
     """
     """
+
     class Meta:
     class Meta:
-        verbose_name = _('Footer')
+        verbose_name = _("Footer")
 
 
     name = models.CharField(
     name = models.CharField(
         max_length=255,
         max_length=255,
-        verbose_name=_('Name'),
+        verbose_name=_("Name"),
     )
     )
     custom_css_class = models.CharField(
     custom_css_class = models.CharField(
         max_length=255,
         max_length=255,
         blank=True,
         blank=True,
-        verbose_name=_('Custom CSS Class'),
+        verbose_name=_("Custom CSS Class"),
     )
     )
     custom_id = models.CharField(
     custom_id = models.CharField(
         max_length=255,
         max_length=255,
         blank=True,
         blank=True,
-        verbose_name=_('Custom ID'),
+        verbose_name=_("Custom ID"),
     )
     )
     content = CoderedStreamField(
     content = CoderedStreamField(
         LAYOUT_STREAMBLOCKS,
         LAYOUT_STREAMBLOCKS,
-        verbose_name=_('Content'),
+        verbose_name=_("Content"),
         blank=True,
         blank=True,
         use_json_field=True,
         use_json_field=True,
     )
     )
 
 
     panels = [
     panels = [
-        FieldPanel('name'),
+        FieldPanel("name"),
         MultiFieldPanel(
         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):
     def __str__(self):
@@ -302,25 +313,23 @@ class ReusableContent(models.Model):
     """
     """
     Snippet for resusable content in streamfields.
     Snippet for resusable content in streamfields.
     """
     """
+
     class Meta:
     class Meta:
-        verbose_name = _('Reusable Content')
-        verbose_name_plural = _('Reusable Content')
+        verbose_name = _("Reusable Content")
+        verbose_name_plural = _("Reusable Content")
 
 
     name = models.CharField(
     name = models.CharField(
         max_length=255,
         max_length=255,
-        verbose_name=_('Name'),
+        verbose_name=_("Name"),
     )
     )
     content = CoderedStreamField(
     content = CoderedStreamField(
         LAYOUT_STREAMBLOCKS,
         LAYOUT_STREAMBLOCKS,
-        verbose_name=_('content'),
+        verbose_name=_("content"),
         blank=True,
         blank=True,
         use_json_field=True,
         use_json_field=True,
     )
     )
 
 
-    panels = [
-        FieldPanel('name'),
-        FieldPanel('content')
-    ]
+    panels = [FieldPanel("name"), FieldPanel("content")]
 
 
     def __str__(self):
     def __str__(self):
         return self.name
         return self.name
@@ -329,26 +338,25 @@ class ReusableContent(models.Model):
 @register_snippet
 @register_snippet
 class Accordion(ClusterableModel):
 class Accordion(ClusterableModel):
     """Class for reusable content in a collapsible block."""
     """Class for reusable content in a collapsible block."""
+
     class Meta:
     class Meta:
-        verbose_name = _('Accordion')
+        verbose_name = _("Accordion")
         verbose_name_plural = _("Accordions")
         verbose_name_plural = _("Accordions")
 
 
     name = models.CharField(
     name = models.CharField(
         max_length=255,
         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):
     def __str__(self):
         return self.name
         return self.name
@@ -359,13 +367,13 @@ class AccordionPanel(Orderable, models.Model):
 
 
     accordion = ParentalKey(
     accordion = ParentalKey(
         Accordion,
         Accordion,
-        related_name='accordion_panels',
-        verbose_name=_('Accordion'),
+        related_name="accordion_panels",
+        verbose_name=_("Accordion"),
     )
     )
 
 
     name = models.CharField(
     name = models.CharField(
         max_length=255,
         max_length=255,
-        verbose_name=_('Name'),
+        verbose_name=_("Name"),
     )
     )
 
 
     content = CoderedStreamField(
     content = CoderedStreamField(
@@ -377,22 +385,20 @@ class AccordionPanel(Orderable, models.Model):
     custom_css_class = models.CharField(
     custom_css_class = models.CharField(
         max_length=255,
         max_length=255,
         blank=True,
         blank=True,
-        verbose_name=_('Custom CSS class'),
+        verbose_name=_("Custom CSS class"),
     )
     )
     custom_id = models.CharField(
     custom_id = models.CharField(
         max_length=255,
         max_length=255,
         blank=True,
         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
 @register_snippet
@@ -400,39 +406,42 @@ class ContentWall(models.Model):
     """
     """
     Snippet that restricts access to a page with a modal.
     Snippet that restricts access to a page with a modal.
     """
     """
+
     class Meta:
     class Meta:
-        verbose_name = _('Content Wall')
+        verbose_name = _("Content Wall")
 
 
     name = models.CharField(
     name = models.CharField(
         max_length=255,
         max_length=255,
-        verbose_name=_('Name'),
+        verbose_name=_("Name"),
     )
     )
     content = CoderedStreamField(
     content = CoderedStreamField(
         LAYOUT_STREAMBLOCKS,
         LAYOUT_STREAMBLOCKS,
-        verbose_name=_('Content'),
+        verbose_name=_("Content"),
         blank=True,
         blank=True,
         use_json_field=True,
         use_json_field=True,
     )
     )
     is_dismissible = models.BooleanField(
     is_dismissible = models.BooleanField(
         default=True,
         default=True,
-        verbose_name=_('Dismissible'),
+        verbose_name=_("Dismissible"),
     )
     )
     show_once = models.BooleanField(
     show_once = models.BooleanField(
         default=True,
         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 = [
     panels = [
         MultiFieldPanel(
         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):
     def __str__(self):
@@ -444,57 +453,62 @@ class CoderedEmail(ClusterableModel):
     General purpose abstract clusterable model used for holding email information.
     General purpose abstract clusterable model used for holding email information.
     Most likely this should be subclassed with addition of a ParentalKey.
     Most likely this should be subclassed with addition of a ParentalKey.
     """
     """
+
     class Meta:
     class Meta:
         abstract = True
         abstract = True
-        verbose_name = _('CodeRed Email')
+        verbose_name = _("CodeRed Email")
 
 
     to_address = models.CharField(
     to_address = models.CharField(
         max_length=255,
         max_length=255,
         blank=True,
         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(
     from_address = models.CharField(
         max_length=255,
         max_length=255,
         blank=True,
         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(
     reply_address = models.CharField(
         max_length=255,
         max_length=255,
         blank=True,
         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(
     cc_address = models.CharField(
         max_length=255,
         max_length=255,
         blank=True,
         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(
     bcc_address = models.CharField(
         max_length=255,
         max_length=255,
         blank=True,
         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):
     def __str__(self):
         return self.subject
         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.navbar = Navbar.objects.create(name="Nav1", custom_id="Nav1")
         self.navbar2 = Navbar.objects.create(name="Nav2", custom_id="Nav2")
         self.navbar2 = Navbar.objects.create(name="Nav2", custom_id="Nav2")
         self.footer = Footer.objects.create(name="Footer1", custom_id="Footer1")
         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.
         # Populate settings.
         self.settings = LayoutSettings.for_site(self.site)
         self.settings = LayoutSettings.for_site(self.site)
@@ -103,10 +105,16 @@ class NavbarFooterTestCase(WagtailPageTests):
         response = self.client.get(self.homepage.url, follow=True)
         response = self.client.get(self.homepage.url, follow=True)
 
 
         self.assertContains(
         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(
         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):
     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,
     CoderedPage,
     CoderedStreamFormPage,
     CoderedStreamFormPage,
     CoderedWebPage,
     CoderedWebPage,
-    get_page_models
+    get_page_models,
 )
 )
 from coderedcms.models.snippet_models import Classifier, ClassifierTerm
 from coderedcms.models.snippet_models import Classifier, ClassifierTerm
 from coderedcms.tests.testapp.models import (
 from coderedcms.tests.testapp.models import (
@@ -25,23 +25,22 @@ from coderedcms.tests.testapp.models import (
     LocationIndexPage,
     LocationIndexPage,
     LocationPage,
     LocationPage,
     StreamFormPage,
     StreamFormPage,
-    WebPage
+    WebPage,
 )
 )
 
 
 
 
-class BasicPageTestCase():
+class BasicPageTestCase:
     """
     """
     This is a testing mixin used to run common tests for basic versions of page types.
     This is a testing mixin used to run common tests for basic versions of page types.
     """
     """
+
     class Meta:
     class Meta:
         abstract = True
         abstract = True
 
 
     def setUp(self):
     def setUp(self):
         self.client = Client()
         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)
         self.homepage.add_child(instance=self.basic_page)
 
 
     def tearDown(self):
     def tearDown(self):
@@ -55,10 +54,11 @@ class BasicPageTestCase():
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
 
 
-class AbstractPageTestCase():
+class AbstractPageTestCase:
     """
     """
     This is a testing mixin used to run common tests for abstract page types.
     This is a testing mixin used to run common tests for abstract page types.
     """
     """
+
     class Meta:
     class Meta:
         abstract = True
         abstract = True
 
 
@@ -71,10 +71,11 @@ class AbstractPageTestCase():
         self.assertFalse(self.model in get_page_models())
         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.
     This is a testing mixin used to run common tests for concrete page types.
     """
     """
+
     class Meta:
     class Meta:
         abstract = True
         abstract = True
 
 
@@ -103,11 +104,7 @@ class ConcreteFormPageTestCase(ConcreteBasicPageTestCase):
         """
         """
         # TODO: add form field via streamfield.
         # TODO: add form field via streamfield.
         response = self.client.post(
         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)
         self.assertEqual(response.status_code, 200)
         # TODO: log in as superuser and get wagtail admin form submission page.
         # 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.
         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(len(messages), 1)
         self.assertEqual(str(messages[0]), self.basic_page.get_spam_message())
         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.
         Test to check if the default spam catching won't mark correct posts as spam.
         """
         """
         response = self.client.post(self.basic_page.url)
         response = self.client.post(self.basic_page.url)
-        self.assertFalse(hasattr(response, 'is_spam'))
+        self.assertFalse(hasattr(response, "is_spam"))
 
 
 
 
 class CoderedArticleIndexPageTestCase(AbstractPageTestCase, WagtailPageTests):
 class CoderedArticleIndexPageTestCase(AbstractPageTestCase, WagtailPageTests):
@@ -219,6 +220,7 @@ class IndexTestCase(ConcreteBasicPageTestCase, WagtailPageTests):
     """
     """
     Tests indexing features (show/sort/filter child pages).
     Tests indexing features (show/sort/filter child pages).
     """
     """
+
     model = IndexTestPage
     model = IndexTestPage
 
 
     def setUp(self):
     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 django.utils.translation import gettext_lazy as _
 from modelcluster.fields import ParentalKey
 from modelcluster.fields import ParentalKey
 from modelcluster.models import ClusterableModel
 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.models import Orderable
 from wagtail.contrib.settings.models import BaseSetting, register_setting
 from wagtail.contrib.settings.models import BaseSetting, register_setting
 from wagtail.images import get_image_model_string
 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
 from coderedcms.models.snippet_models import Navbar, Footer
 
 
 
 
-@register_setting(icon='cr-desktop')
+@register_setting(icon="cr-desktop")
 class LayoutSettings(ClusterableModel, BaseSetting):
 class LayoutSettings(ClusterableModel, BaseSetting):
     """
     """
     Branding, navbar, and theme settings.
     Branding, navbar, and theme settings.
     """
     """
+
     class Meta:
     class Meta:
-        verbose_name = _('Layout')
+        verbose_name = _("Layout")
 
 
     logo = models.ForeignKey(
     logo = models.ForeignKey(
         get_image_model_string(),
         get_image_model_string(),
         null=True,
         null=True,
         blank=True,
         blank=True,
         on_delete=models.SET_NULL,
         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(
     favicon = models.ForeignKey(
         get_image_model_string(),
         get_image_model_string(),
         null=True,
         null=True,
         blank=True,
         blank=True,
         on_delete=models.SET_NULL,
         on_delete=models.SET_NULL,
-        related_name='favicon',
-        verbose_name=_('Favicon'),
+        related_name="favicon",
+        verbose_name=_("Favicon"),
     )
     )
     navbar_color_scheme = models.CharField(
     navbar_color_scheme = models.CharField(
         blank=True,
         blank=True,
         max_length=50,
         max_length=50,
         choices=None,
         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(
     navbar_class = models.CharField(
         blank=True,
         blank=True,
         max_length=255,
         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(
     navbar_fixed = models.BooleanField(
         default=False,
         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(
     navbar_content_fluid = models.BooleanField(
         default=False,
         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(
     navbar_collapse_mode = models.CharField(
         blank=True,
         blank=True,
         max_length=50,
         max_length=50,
         choices=None,
         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(
     navbar_format = models.CharField(
         blank=True,
         blank=True,
         max_length=50,
         max_length=50,
         choices=None,
         choices=None,
-        default='',
-        verbose_name=_('Navbar format'),
+        default="",
+        verbose_name=_("Navbar format"),
     )
     )
     navbar_search = models.BooleanField(
     navbar_search = models.BooleanField(
         default=True,
         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(
     frontend_theme = models.CharField(
         blank=True,
         blank=True,
         max_length=50,
         max_length=50,
         choices=None,
         choices=None,
-        default='',
-        verbose_name=_('Theme variant'),
+        default="",
+        verbose_name=_("Theme variant"),
     )
     )
 
 
     panels = [
     panels = [
         MultiFieldPanel(
         MultiFieldPanel(
             [
             [
-                FieldPanel('logo'),
-                FieldPanel('favicon'),
+                FieldPanel("logo"),
+                FieldPanel("favicon"),
             ],
             ],
-            heading=_('Branding')
+            heading=_("Branding"),
         ),
         ),
         InlinePanel(
         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(
         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(
         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(
         MultiFieldPanel(
             [
             [
-                FieldPanel('frontend_theme'),
+                FieldPanel("frontend_theme"),
             ],
             ],
-            heading=_('Theming')
+            heading=_("Theming"),
         ),
         ),
     ]
     ]
 
 
@@ -140,24 +155,28 @@ class LayoutSettings(ClusterableModel, BaseSetting):
         """
         """
         super().__init__(*args, **kwargs)
         super().__init__(*args, **kwargs)
         # Set choices dynamically.
         # 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.
         # Set default dynamically.
         if not self.id:
         if not self.id:
             self.frontend_theme = crx_settings.CRX_FRONTEND_THEME_DEFAULT
             self.frontend_theme = crx_settings.CRX_FRONTEND_THEME_DEFAULT
             self.navbar_class = crx_settings.CRX_FRONTEND_NAVBAR_CLASS_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
             self.navbar_format = crx_settings.CRX_FRONTEND_NAVBAR_FORMAT_DEFAULT
 
 
 
 
@@ -165,7 +184,7 @@ class NavbarOrderable(Orderable, models.Model):
     navbar_chooser = ParentalKey(
     navbar_chooser = ParentalKey(
         LayoutSettings,
         LayoutSettings,
         related_name="site_navbar",
         related_name="site_navbar",
-        verbose_name=_('Site Navbars')
+        verbose_name=_("Site Navbars"),
     )
     )
     navbar = models.ForeignKey(
     navbar = models.ForeignKey(
         Navbar,
         Navbar,
@@ -174,16 +193,14 @@ class NavbarOrderable(Orderable, models.Model):
         on_delete=models.CASCADE,
         on_delete=models.CASCADE,
     )
     )
 
 
-    panels = [
-        FieldPanel("navbar")
-    ]
+    panels = [FieldPanel("navbar")]
 
 
 
 
 class FooterOrderable(Orderable, models.Model):
 class FooterOrderable(Orderable, models.Model):
     footer_chooser = ParentalKey(
     footer_chooser = ParentalKey(
         LayoutSettings,
         LayoutSettings,
         related_name="site_footer",
         related_name="site_footer",
-        verbose_name=_('Site Footers')
+        verbose_name=_("Site Footers"),
     )
     )
     footer = models.ForeignKey(
     footer = models.ForeignKey(
         Footer,
         Footer,
@@ -192,115 +209,124 @@ class FooterOrderable(Orderable, models.Model):
         on_delete=models.CASCADE,
         on_delete=models.CASCADE,
     )
     )
 
 
-    panels = [
-        FieldPanel("footer")
-    ]
+    panels = [FieldPanel("footer")]
 
 
 
 
-@register_setting(icon='cr-google')
+@register_setting(icon="cr-google")
 class AnalyticsSettings(BaseSetting):
 class AnalyticsSettings(BaseSetting):
     """
     """
     Tracking and Google Analytics.
     Tracking and Google Analytics.
     """
     """
+
     class Meta:
     class Meta:
-        verbose_name = _('Tracking')
+        verbose_name = _("Tracking")
 
 
     ga_tracking_id = models.CharField(
     ga_tracking_id = models.CharField(
         blank=True,
         blank=True,
         max_length=255,
         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(
     ga_g_tracking_id = models.CharField(
         blank=True,
         blank=True,
         max_length=255,
         max_length=255,
-        verbose_name=_('G Tracking ID'),
+        verbose_name=_("G Tracking ID"),
         help_text=_('Your Google Analytics 4 tracking ID (begins with "G-")'),
         help_text=_('Your Google Analytics 4 tracking ID (begins with "G-")'),
     )
     )
     ga_track_button_clicks = models.BooleanField(
     ga_track_button_clicks = models.BooleanField(
         default=False,
         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(
     gtm_id = models.CharField(
         blank=True,
         blank=True,
         max_length=255,
         max_length=255,
-        verbose_name=_('Google Tag Manager ID'),
+        verbose_name=_("Google Tag Manager ID"),
         help_text=_('Begins with "GTM-"'),
         help_text=_('Begins with "GTM-"'),
     )
     )
     head_scripts = MonospaceField(
     head_scripts = MonospaceField(
         blank=True,
         blank=True,
         null=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(
     body_scripts = MonospaceField(
         blank=True,
         blank=True,
         null=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 = [
     panels = [
         HelpPanel(
         HelpPanel(
-            heading=_('Know your tracking'),
+            heading=_("Know your tracking"),
             content=_(
             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
                 '<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, '
                 'target="_blank">read about the difference between UA, G, GTM, '
-                'and other tracking IDs</a>.</p>'
+                "and other tracking IDs</a>.</p>"
             ),
             ),
         ),
         ),
         MultiFieldPanel(
         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(
         MultiFieldPanel(
             [
             [
-                FieldPanel('gtm_id'),
+                FieldPanel("gtm_id"),
             ],
             ],
-            heading=_('Google Tag Manager'),
+            heading=_("Google Tag Manager"),
         ),
         ),
         MultiFieldPanel(
         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):
 class ADASettings(BaseSetting):
     """
     """
     Accessibility related options.
     Accessibility related options.
     """
     """
+
     class Meta:
     class Meta:
-        verbose_name = 'Accessibility'
+        verbose_name = "Accessibility"
 
 
     skip_navigation = models.BooleanField(
     skip_navigation = models.BooleanField(
         default=False,
         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 = [
     panels = [
         MultiFieldPanel(
         MultiFieldPanel(
             [
             [
-                FieldPanel('skip_navigation'),
+                FieldPanel("skip_navigation"),
             ],
             ],
-            heading=_('Accessibility')
+            heading=_("Accessibility"),
         )
         )
     ]
     ]
 
 
 
 
-@register_setting(icon='cog')
+@register_setting(icon="cog")
 class GeneralSettings(BaseSetting):
 class GeneralSettings(BaseSetting):
     """
     """
     Various site-wide settings. A good place to put
     Various site-wide settings. A good place to put
@@ -308,73 +334,77 @@ class GeneralSettings(BaseSetting):
     """
     """
 
 
     from_email_address = models.CharField(
     from_email_address = models.CharField(
-
         blank=True,
         blank=True,
         max_length=255,
         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(
     search_num_results = models.PositiveIntegerField(
         default=10,
         default=10,
-        verbose_name=_('Number of results per page'),
+        verbose_name=_("Number of results per page"),
     )
     )
     external_new_tab = models.BooleanField(
     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 = [
     panels = [
         MultiFieldPanel(
         MultiFieldPanel(
             [
             [
-                FieldPanel('from_email_address'),
+                FieldPanel("from_email_address"),
             ],
             ],
-            _('Email')
+            _("Email"),
         ),
         ),
         MultiFieldPanel(
         MultiFieldPanel(
             [
             [
-                FieldPanel('search_num_results'),
+                FieldPanel("search_num_results"),
             ],
             ],
-            _('Search Settings')
+            _("Search Settings"),
         ),
         ),
         MultiFieldPanel(
         MultiFieldPanel(
             [
             [
-                FieldPanel('external_new_tab'),
+                FieldPanel("external_new_tab"),
             ],
             ],
-            _('Links')
+            _("Links"),
         ),
         ),
     ]
     ]
 
 
     class Meta:
     class Meta:
-        verbose_name = _('General')
+        verbose_name = _("General")
 
 
 
 
-@register_setting(icon='cr-puzzle-piece')
+@register_setting(icon="cr-puzzle-piece")
 class GoogleApiSettings(BaseSetting):
 class GoogleApiSettings(BaseSetting):
     """
     """
     Settings for Google API services.
     Settings for Google API services.
     """
     """
+
     class Meta:
     class Meta:
-        verbose_name = _('Google API')
+        verbose_name = _("Google API")
 
 
     google_maps_api_key = models.CharField(
     google_maps_api_key = models.CharField(
         blank=True,
         blank=True,
         max_length=255,
         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):
 class MailchimpApiSettings(BaseSetting):
     """
     """
     Settings for Mailchimp API services.
     Settings for Mailchimp API services.
     """
     """
+
     class Meta:
     class Meta:
-        verbose_name = _('Mailchimp API')
+        verbose_name = _("Mailchimp API")
 
 
     mailchimp_api_key = models.CharField(
     mailchimp_api_key = models.CharField(
         blank=True,
         blank=True,
         max_length=255,
         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
 import sys
 
 
 if __name__ == "__main__":
 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
     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 = [
 INSTALLED_APPS = [
     # This project
     # This project
-    'website',
-
+    "website",
     # Wagtail CRX (CodeRed Extensions)
     # Wagtail CRX (CodeRed Extensions)
-    'coderedcms',
-    'django_bootstrap5',
-    'modelcluster',
-    'taggit',
-    'wagtailcache',
-    'wagtailseo',
-
+    "coderedcms",
+    "django_bootstrap5",
+    "modelcluster",
+    "taggit",
+    "wagtailcache",
+    "wagtailseo",
     # Wagtail
     # 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
-    '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 = [
 MIDDLEWARE = [
     # Save pages to cache. Must be FIRST.
     # Save pages to cache. Must be FIRST.
-    'wagtailcache.cache.UpdateCacheMiddleware',
-
+    "wagtailcache.cache.UpdateCacheMiddleware",
     # Common functionality
     # 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
     # 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.
     #  Error reporting. Uncomment this to receive emails when a 404 is triggered.
     # 'django.middleware.common.BrokenLinkEmailsMiddleware',
     # 'django.middleware.common.BrokenLinkEmailsMiddleware',
-
     # CMS functionality
     # CMS functionality
-    'wagtail.contrib.redirects.middleware.RedirectMiddleware',
-
+    "wagtail.contrib.redirects.middleware.RedirectMiddleware",
     # Fetch from cache. Must be LAST.
     # Fetch from cache. Must be LAST.
-    'wagtailcache.cache.FetchFromCacheMiddleware',
+    "wagtailcache.cache.FetchFromCacheMiddleware",
 ]
 ]
 
 
-ROOT_URLCONF = '{{ project_name }}.urls'
+ROOT_URLCONF = "{{ project_name }}.urls"
 
 
 TEMPLATES = [
 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
 # Database
 # https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#databases
 # https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#databases
 
 
 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 = [
 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/
 # https://docs.djangoproject.com/en/{{ docs_version }}/topics/i18n/
 
 
 # To add or change language of the project, modify the list below.
 # 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
 USE_I18N = True
 
 
@@ -158,38 +148,38 @@ USE_TZ = True
 # https://docs.djangoproject.com/en/{{ docs_version }}/howto/static-files/
 # https://docs.djangoproject.com/en/{{ docs_version }}/howto/static-files/
 
 
 STATICFILES_FINDERS = [
 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
 
 
-LOGIN_URL = 'wagtailadmin_login'
-LOGIN_REDIRECT_URL = 'wagtailadmin_home'
+LOGIN_URL = "wagtailadmin_login"
+LOGIN_REDIRECT_URL = "wagtailadmin_home"
 
 
 
 
 # Wagtail settings
 # Wagtail settings
 
 
-WAGTAIL_SITE_NAME = '{{ sitename }}'
+WAGTAIL_SITE_NAME = "{{ sitename }}"
 
 
 WAGTAIL_ENABLE_UPDATE_CHECK = False
 WAGTAIL_ENABLE_UPDATE_CHECK = False
 
 
 WAGTAILSEARCH_BACKENDS = {
 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 -
 # 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
 # e.g. in notification emails. Don't include '/admin' or a trailing slash
-WAGTAILADMIN_BASE_URL = 'http://{{ domain }}'
+WAGTAILADMIN_BASE_URL = "http://{{ domain }}"
 
 
 
 
 # Tags
 # Tags
@@ -199,4 +189,4 @@ TAGGIT_CASE_INSENSITIVE = True
 
 
 # Sets default for primary key IDs
 # Sets default for primary key IDs
 # See https://docs.djangoproject.com/en/{{ docs_version }}/ref/models/fields/#bigautofield
 # 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
 DEBUG = True
 
 
 # SECURITY WARNING: keep the secret key used in production secret!
 # 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
 WAGTAIL_CACHE = False
 
 

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

@@ -4,22 +4,22 @@ from .base import *  # noqa
 DEBUG = False
 DEBUG = False
 
 
 # SECURITY WARNING: keep the secret key used in production secret!
 # 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.
 # 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
 # To send email from the server, we recommend django_sendmail_backend
 # Or specify your own email backend such as an SMTP server.
 # Or specify your own email backend such as an SMTP server.
 # https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#email-backend
 # 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 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.
 # A list of people who get error notifications.
 ADMINS = [
 ADMINS = [
-    ('Administrator', 'admin@{{ domain_nowww }}'),
+    ("Administrator", "admin@{{ domain_nowww }}"),
 ]
 ]
 
 
 # A list in the same format as ADMINS that specifies who should get broken link
 # 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.
 # Requires reloading web server to pick up template changes.
 TEMPLATES = [
 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 = {
 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 = [
 urlpatterns = [
     # Admin
     # Admin
-    path('django-admin/', admin.site.urls),
-    path('admin/', include(coderedadmin_urls)),
-
+    path("django-admin/", admin.site.urls),
+    path("admin/", include(coderedadmin_urls)),
     # Documents
     # Documents
-    path('docs/', include(wagtaildocs_urls)),
-
+    path("docs/", include(wagtaildocs_urls)),
     # Search
     # Search
-    path('search/', include(coderedsearch_urls)),
-
+    path("search/", include(coderedsearch_urls)),
     # For anything not caught by a more specific rule above, hand over to
     # 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 page serving mechanism. This should be the last pattern in
     # the list:
     # the list:
-    path('', include(codered_urls)),
-
+    path("", include(codered_urls)),
     # Alternatively, if you want CMS pages to be served from a subpath
     # Alternatively, if you want CMS pages to be served from a subpath
     # of your site, rather than the site root:
     # of your site, rather than the site root:
     #    path("pages/", include(codered_urls)),
     #    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
 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()
 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):
 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,
     CoderedArticleIndexPage,
     CoderedEmail,
     CoderedEmail,
     CoderedFormPage,
     CoderedFormPage,
-    CoderedWebPage
+    CoderedWebPage,
 )
 )
 
 
 
 
@@ -16,65 +16,71 @@ class ArticlePage(CoderedArticlePage):
     """
     """
     Article, suitable for news or blog content.
     Article, suitable for news or blog content.
     """
     """
+
     class Meta:
     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.
     # 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):
 class ArticleIndexPage(CoderedArticleIndexPage):
     """
     """
     Shows a list of article sub-pages.
     Shows a list of article sub-pages.
     """
     """
+
     class Meta:
     class Meta:
-        verbose_name = 'Article Landing Page'
+        verbose_name = "Article Landing Page"
 
 
     # Override to specify custom index ordering choice/default.
     # Override to specify custom index ordering choice/default.
-    index_query_pagemodel = 'website.ArticlePage'
+    index_query_pagemodel = "website.ArticlePage"
 
 
     # Only allow ArticlePages beneath this page.
     # 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):
 class FormPage(CoderedFormPage):
     """
     """
     A page with an html <form>.
     A page with an html <form>.
     """
     """
+
     class Meta:
     class Meta:
-        verbose_name = 'Form'
+        verbose_name = "Form"
 
 
-    template = 'coderedcms/pages/form_page.html'
+    template = "coderedcms/pages/form_page.html"
 
 
 
 
 class FormPageField(CoderedFormField):
 class FormPageField(CoderedFormField):
     """
     """
     A field that links to a FormPage.
     A field that links to a FormPage.
     """
     """
+
     class Meta:
     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):
 class FormConfirmEmail(CoderedEmail):
     """
     """
     Sends a confirmation email after submitting a FormPage.
     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):
 class WebPage(CoderedWebPage):
     """
     """
     General use page with featureful streamfield and SEO attributes.
     General use page with featureful streamfield and SEO attributes.
     """
     """
+
     class Meta:
     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
 import sys
 
 
 if __name__ == "__main__":
 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
     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 = [
 INSTALLED_APPS = [
     # This project
     # This project
-    'website',
-
+    "website",
     # Wagtail CRX (CodeRed Extensions)
     # Wagtail CRX (CodeRed Extensions)
-    'coderedcms',
-    'django_bootstrap5',
-    'modelcluster',
-    'taggit',
-    'wagtailcache',
-    'wagtailseo',
-
+    "coderedcms",
+    "django_bootstrap5",
+    "modelcluster",
+    "taggit",
+    "wagtailcache",
+    "wagtailseo",
     # Wagtail
     # 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
-    '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",
     "django.contrib.sitemaps",
 ]
 ]
 
 
 MIDDLEWARE = [
 MIDDLEWARE = [
     # Save pages to cache. Must be FIRST.
     # Save pages to cache. Must be FIRST.
-    'wagtailcache.cache.UpdateCacheMiddleware',
-
+    "wagtailcache.cache.UpdateCacheMiddleware",
     # Common functionality
     # 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
     # 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.
     # Error reporting. Uncomment this to receive emails when a 404 is triggered.
-    #'django.middleware.common.BrokenLinkEmailsMiddleware',
-
+    # 'django.middleware.common.BrokenLinkEmailsMiddleware',
     # CMS functionality
     # CMS functionality
-    'wagtail.contrib.redirects.middleware.RedirectMiddleware',
-
+    "wagtail.contrib.redirects.middleware.RedirectMiddleware",
     # Fetch from cache. Must be LAST.
     # Fetch from cache. Must be LAST.
-    'wagtailcache.cache.FetchFromCacheMiddleware',
+    "wagtailcache.cache.FetchFromCacheMiddleware",
 ]
 ]
 
 
-ROOT_URLCONF = '{{ project_name }}.urls'
+ROOT_URLCONF = "{{ project_name }}.urls"
 
 
 TEMPLATES = [
 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
 # Database
 # https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#databases
 # https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#databases
 
 
 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 = [
 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
 # Internationalization
 # https://docs.djangoproject.com/en/{{ docs_version }}/topics/i18n/
 # 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
 USE_I18N = False
 
 
@@ -153,21 +145,21 @@ USE_TZ = True
 # https://docs.djangoproject.com/en/{{ docs_version }}/howto/static-files/
 # https://docs.djangoproject.com/en/{{ docs_version }}/howto/static-files/
 
 
 STATICFILES_FINDERS = [
 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
 
 
-LOGIN_URL = 'wagtailadmin_login'
-LOGIN_REDIRECT_URL = 'wagtailadmin_home'
+LOGIN_URL = "wagtailadmin_login"
+LOGIN_REDIRECT_URL = "wagtailadmin_home"
 
 
 
 
 # Wagtail settings
 # Wagtail settings
@@ -177,14 +169,14 @@ WAGTAIL_SITE_NAME = "{{ sitename }}"
 WAGTAIL_ENABLE_UPDATE_CHECK = False
 WAGTAIL_ENABLE_UPDATE_CHECK = False
 
 
 WAGTAILSEARCH_BACKENDS = {
 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 -
 # 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
 # e.g. in notification emails. Don't include '/admin' or a trailing slash
-WAGTAILADMIN_BASE_URL = 'http://{{ domain }}'
+WAGTAILADMIN_BASE_URL = "http://{{ domain }}"
 
 
 
 
 # Tags
 # Tags
@@ -194,4 +186,4 @@ TAGGIT_CASE_INSENSITIVE = True
 
 
 # Sets default for primary key IDs
 # Sets default for primary key IDs
 # See https://docs.djangoproject.com/en/{{ docs_version }}/ref/models/fields/#bigautofield
 # 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!
 # SECURITY WARNING: don't run with debug turned on in production!
 DEBUG = True
 DEBUG = True
 
 
 # SECURITY WARNING: keep the secret key used in production secret!
 # 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
 WAGTAIL_CACHE = False
 
 
 try:
 try:
-    from .local_settings import *
+    from .local import *  # noqa
 except ImportError:
 except ImportError:
     pass
     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!
 # SECURITY WARNING: don't run with debug turned on in production!
 DEBUG = False
 DEBUG = False
 
 
 # SECURITY WARNING: keep the secret key used in production secret!
 # 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.
 # 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
 # To send email from the server, we recommend django_sendmail_backend
 # Or specify your own email backend such as an SMTP server.
 # Or specify your own email backend such as an SMTP server.
 # https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#email-backend
 # 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 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.
 # A list of people who get error notifications.
 ADMINS = [
 ADMINS = [
-    ('Administrator', 'admin@{{ domain_nowww }}'),
+    ("Administrator", "admin@{{ domain_nowww }}"),
 ]
 ]
 
 
 # A list in the same format as ADMINS that specifies who should get broken link
 # 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.
 # Email address used to send error messages to ADMINS.
 SERVER_EMAIL = DEFAULT_FROM_EMAIL
 SERVER_EMAIL = DEFAULT_FROM_EMAIL
 
 
-#DATABASES = {
+# DATABASES = {
 #    'default': {
 #    'default': {
 #        'ENGINE': 'django.db.backends.mysql',
 #        'ENGINE': 'django.db.backends.mysql',
 #        'HOST': 'localhost',
 #        'HOST': 'localhost',
@@ -37,41 +37,39 @@ SERVER_EMAIL = DEFAULT_FROM_EMAIL
 #        'USER': '{{ project_name }}',
 #        'USER': '{{ project_name }}',
 #        'PASSWORD': '',
 #        'PASSWORD': '',
 #    }
 #    }
-#}
+# }
 
 
 # Use template caching to speed up wagtail admin and front-end.
 # Use template caching to speed up wagtail admin and front-end.
 # Requires reloading web server to pick up template changes.
 # Requires reloading web server to pick up template changes.
 TEMPLATES = [
 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 = {
 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 = [
 urlpatterns = [
     # Admin
     # Admin
-    path('django-admin/', admin.site.urls),
-    path('admin/', include(coderedadmin_urls)),
-
+    path("django-admin/", admin.site.urls),
+    path("admin/", include(coderedadmin_urls)),
     # Documents
     # Documents
-    path('docs/', include(wagtaildocs_urls)),
-
+    path("docs/", include(wagtaildocs_urls)),
     # Search
     # Search
-    path('search/', include(coderedsearch_urls)),
-
+    path("search/", include(coderedsearch_urls)),
     # For anything not caught by a more specific rule above, hand over to
     # 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 page serving mechanism. This should be the last pattern in
     # the list:
     # the list:
-    path('', include(codered_urls)),
-
+    path("", include(codered_urls)),
     # Alternatively, if you want CMS pages to be served from a subpath
     # Alternatively, if you want CMS pages to be served from a subpath
     # of your site, rather than the site root:
     # of your site, rather than the site root:
     #    path("pages/", include(codered_urls)),
     #    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
 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()
 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):
 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,
     CoderedArticleIndexPage,
     CoderedEmail,
     CoderedEmail,
     CoderedFormPage,
     CoderedFormPage,
-    CoderedWebPage
+    CoderedWebPage,
 )
 )
 
 
 
 
@@ -16,65 +16,71 @@ class ArticlePage(CoderedArticlePage):
     """
     """
     Article, suitable for news or blog content.
     Article, suitable for news or blog content.
     """
     """
+
     class Meta:
     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.
     # 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):
 class ArticleIndexPage(CoderedArticleIndexPage):
     """
     """
     Shows a list of article sub-pages.
     Shows a list of article sub-pages.
     """
     """
+
     class Meta:
     class Meta:
-        verbose_name = 'Article Landing Page'
+        verbose_name = "Article Landing Page"
 
 
     # Override to specify custom index ordering choice/default.
     # Override to specify custom index ordering choice/default.
-    index_query_pagemodel = 'website.ArticlePage'
+    index_query_pagemodel = "website.ArticlePage"
 
 
     # Only allow ArticlePages beneath this page.
     # 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):
 class FormPage(CoderedFormPage):
     """
     """
     A page with an html <form>.
     A page with an html <form>.
     """
     """
+
     class Meta:
     class Meta:
-        verbose_name = 'Form'
+        verbose_name = "Form"
 
 
-    template = 'coderedcms/pages/form_page.html'
+    template = "coderedcms/pages/form_page.html"
 
 
 
 
 class FormPageField(CoderedFormField):
 class FormPageField(CoderedFormField):
     """
     """
     A field that links to a FormPage.
     A field that links to a FormPage.
     """
     """
+
     class Meta:
     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):
 class FormConfirmEmail(CoderedEmail):
     """
     """
     Sends a confirmation email after submitting a FormPage.
     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):
 class WebPage(CoderedWebPage):
     """
     """
     General use page with featureful streamfield and SEO attributes.
     General use page with featureful streamfield and SEO attributes.
     """
     """
+
     class Meta:
     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
 from coderedcms.views import search
 
 
 urlpatterns = [
 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:
 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_WHITELIST = []
     CRX_PROTECTED_MEDIA_UPLOAD_BLACKLIST = [
     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 = [
     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 = [
     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 = [
     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 = [
     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 = [
     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 = [
     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 = [
     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 = [
     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 = {
     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
         # templates that are available for all block types
-        '*': [
-            ('', 'Default'),
+        "*": [
+            ("", "Default"),
         ],
         ],
     }
     }
 
 
     CRX_FRONTEND_TEMPLATES_PAGES = {
     CRX_FRONTEND_TEMPLATES_PAGES = {
         # templates that are available for all page types
         # 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 = None
-    CRX_BANNER_BACKGROUND = '#f00'
-    CRX_BANNER_TEXT_COLOR = '#fff'
+    CRX_BANNER_BACKGROUND = "#f00"
+    CRX_BANNER_TEXT_COLOR = "#fff"
 
 
     def __getattribute__(self, attr: str):
     def __getattribute__(self, attr: str):
         # First load from Django settings.
         # 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,
 # If the older django-bootstrap4 is the only version listed in INSTALLED_APPS,
 # use it for compatibility. Otherwise use django-bootstrap5 which is a
 # use it for compatibility. Otherwise use django-bootstrap5 which is a
 # dependency of coderedcms.
 # 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
     import bootstrap4.bootstrap as bootstrap
 else:
 else:

+ 45 - 41
coderedcms/templatetags/coderedcms_tags.py

@@ -27,7 +27,9 @@ def is_advanced_setting(obj):
 
 
 @register.filter
 @register.filter
 def is_file_form(form):
 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
 @register.simple_tag
@@ -37,25 +39,25 @@ def coderedcms_version():
 
 
 @register.simple_tag
 @register.simple_tag
 def generate_random_id():
 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)
     return "cr-{}".format(value)
 
 
 
 
 @register.simple_tag
 @register.simple_tag
 def is_menu_item_dropdown(value):
 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)
 @register.simple_tag(takes_context=True)
 def is_active_page(context, curr_page, other_page):
 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 curr_url == other_url
     return False
     return False
 
 
@@ -68,34 +70,36 @@ def get_pictures(collection_id):
 
 
 @register.simple_tag(takes_context=True)
 @register.simple_tag(takes_context=True)
 def get_navbar_css(context):
 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 ""
     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)
 @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()
     navbarorderables = layout.site_navbar.all()
     navbars = Navbar.objects.filter(
     navbars = Navbar.objects.filter(
         navbarorderable__in=navbarorderables
         navbarorderable__in=navbarorderables
-        ).order_by('navbarorderable__sort_order')
+    ).order_by("navbarorderable__sort_order")
     return navbars
     return navbars
 
 
 
 
 @register.simple_tag(takes_context=True)
 @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()
     footerorderables = layout.site_footer.all()
     footers = Footer.objects.filter(
     footers = Footer.objects.filter(
         footerorderable__in=footerorderables
         footerorderable__in=footerorderables
-        ).order_by('footerorderable__sort_order')
+    ).order_by("footerorderable__sort_order")
     return footers
     return footers
 
 
 
 
@@ -113,7 +117,9 @@ def get_pageform(page, request):
 
 
 @register.simple_tag
 @register.simple_tag
 def process_form_cell(request, cell):
 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)
         return utils.get_protected_media_link(request, cell, render_link=True)
     if utils.uri_validator(str(cell)):
     if utils.uri_validator(str(cell)):
         return mark_safe("<a href='{0}'>{1}</a>".format(cell, 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
             get[key] = value
         else:
         else:
             try:
             try:
-                del(get[key])
+                del get[key]
             except KeyError:
             except KeyError:
                 pass
                 pass
     return get
     return get
@@ -156,8 +162,8 @@ def query_update(querydict, key=None, value=None):
 def render_iframe_from_embed(embed):
 def render_iframe_from_embed(embed):
     soup = BeautifulSoup(embed.html, "html.parser")
     soup = BeautifulSoup(embed.html, "html.parser")
     try:
     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())
         return mark_safe(soup.prettify())
     except AttributeError:
     except AttributeError:
         pass
         pass
@@ -173,26 +179,25 @@ def map_to_bootstrap_alert(message_tag):
     Converts a message level to a bootstrap 4 alert class
     Converts a message level to a bootstrap 4 alert class
     """
     """
     message_to_alert_dict = {
     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:
     try:
         return message_to_alert_dict[message_tag]
         return message_to_alert_dict[message_tag]
     except KeyError:
     except KeyError:
-        return ''
+        return ""
 
 
 
 
 @register.filter
 @register.filter
 def get_name_of_class(class_type):
 def get_name_of_class(class_type):
     if hasattr(class_type.__class__, "search_name"):
     if hasattr(class_type.__class__, "search_name"):
         return 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
         return class_type.__class__._meta.verbose_name
     else:
     else:
@@ -203,9 +208,8 @@ def get_name_of_class(class_type):
 def get_plural_name_of_class(class_type):
 def get_plural_name_of_class(class_type):
     if hasattr(class_type.__class__, "search_name_plural"):
     if hasattr(class_type.__class__, "search_name_plural"):
         return 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
         return class_type.__class__._meta.verbose_name_plural
     else:
     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, ...)
 # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
 import os
 import os
-from django.utils.translation import gettext_lazy as _  # noqa
 
 
 
 
 PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 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 = [
 INSTALLED_APPS = [
     # Test
     # Test
-    'coderedcms.tests.testapp',
-
+    "coderedcms.tests.testapp",
     # Wagtail CRX (CodeRed Extensions)
     # Wagtail CRX (CodeRed Extensions)
-    'coderedcms',
-    'django_bootstrap5',
-    'modelcluster',
-    'taggit',
-    'wagtailcache',
-    'wagtailseo',
-
+    "coderedcms",
+    "django_bootstrap5",
+    "modelcluster",
+    "taggit",
+    "wagtailcache",
+    "wagtailseo",
     # Wagtail
     # 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
-    '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",
     "django.contrib.sitemaps",
 ]
 ]
 
 
 MIDDLEWARE = [
 MIDDLEWARE = [
     # Save pages to cache. Must be FIRST.
     # Save pages to cache. Must be FIRST.
-    'wagtailcache.cache.UpdateCacheMiddleware',
-
+    "wagtailcache.cache.UpdateCacheMiddleware",
     # Common functionality
     # 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
     # 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.
     # Error reporting. Uncomment this to receive emails when a 404 is triggered.
     # 'django.middleware.common.BrokenLinkEmailsMiddleware',
     # 'django.middleware.common.BrokenLinkEmailsMiddleware',
-
     # CMS functionality
     # CMS functionality
-    'wagtail.contrib.redirects.middleware.RedirectMiddleware',
-
+    "wagtail.contrib.redirects.middleware.RedirectMiddleware",
     # Fetch from cache. Must be LAST.
     # Fetch from cache. Must be LAST.
-    'wagtailcache.cache.FetchFromCacheMiddleware',
+    "wagtailcache.cache.FetchFromCacheMiddleware",
 ]
 ]
 
 
-ROOT_URLCONF = 'coderedcms.tests.urls'
+ROOT_URLCONF = "coderedcms.tests.urls"
 
 
 TEMPLATES = [
 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
 # https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#databases
 
 
 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 = [
 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
 # Internationalization
 # https://docs.djangoproject.com/en/{{ docs_version }}/topics/i18n/
 # 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
 USE_I18N = True
 
 
@@ -156,21 +147,21 @@ USE_TZ = True
 # https://docs.djangoproject.com/en/{{ docs_version }}/howto/static-files/
 # https://docs.djangoproject.com/en/{{ docs_version }}/howto/static-files/
 
 
 STATICFILES_FINDERS = [
 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
 
 
-LOGIN_URL = 'wagtailadmin_login'
-LOGIN_REDIRECT_URL = 'wagtailadmin_home'
+LOGIN_URL = "wagtailadmin_login"
+LOGIN_REDIRECT_URL = "wagtailadmin_home"
 
 
 
 
 # Wagtail settings
 # Wagtail settings
@@ -180,27 +171,29 @@ WAGTAIL_SITE_NAME = ""
 WAGTAIL_ENABLE_UPDATE_CHECK = False
 WAGTAIL_ENABLE_UPDATE_CHECK = False
 
 
 WAGTAILSEARCH_BACKENDS = {
 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 -
 # 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
 # e.g. in notification emails. Don't include '/admin' or a trailing slash
-WAGTAILADMIN_BASE_URL = ''
+WAGTAILADMIN_BASE_URL = ""
 
 
 
 
 # Tags
 # Tags
 
 
 TAGGIT_CASE_INSENSITIVE = True
 TAGGIT_CASE_INSENSITIVE = True
 
 
-EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
+EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
 
 
 WAGTAIL_CACHE = False
 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):
 class TestCoderedcmsStart(unittest.TestCase):
     CURR_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
     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):
     def setup(self):
         # Clean/create directory to start into
         # Clean/create directory to start into
@@ -22,14 +22,14 @@ class TestCoderedcmsStart(unittest.TestCase):
 
 
     def test_help(self):
     def test_help(self):
         # Set args
         # Set args
-        sys.argv = ['coderedcms', 'help']
+        sys.argv = ["coderedcms", "help"]
         # Run
         # Run
         coderedcms_main()
         coderedcms_main()
         # Nothing to assert here... just make sure it doesn't error out.
         # Nothing to assert here... just make sure it doesn't error out.
 
 
     def test_help_start(self):
     def test_help_start(self):
         # Set args
         # Set args
-        sys.argv = ['coderedcms', 'help', 'start']
+        sys.argv = ["coderedcms", "help", "start"]
         # Run
         # Run
         coderedcms_main()
         coderedcms_main()
         # Nothing to assert here... just make sure it doesn't error out.
         # Nothing to assert here... just make sure it doesn't error out.
@@ -37,53 +37,72 @@ class TestCoderedcmsStart(unittest.TestCase):
     def test_default(self):
     def test_default(self):
         self.setup()
         self.setup()
         # Set args
         # Set args
-        sys.argv = ['coderedcms', 'start', 'myproject', self.TEST_DIR]
+        sys.argv = ["coderedcms", "start", "myproject", self.TEST_DIR]
         # Run
         # Run
         coderedcms_main()
         coderedcms_main()
         # Assert files exist
         # 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()
         self.cleanup()
 
 
     def test_allopts(self):
     def test_allopts(self):
         self.setup()
         self.setup()
         # Set args
         # Set args
         sys.argv = [
         sys.argv = [
-            'coderedcms',
-            'start',
-            'myproject',
+            "coderedcms",
+            "start",
+            "myproject",
             self.TEST_DIR,
             self.TEST_DIR,
-            '--template', 'basic',
-            '--sitename', 'MegaCorp, Inc.',
-            '--domain', 'example.com'
+            "--template",
+            "basic",
+            "--sitename",
+            "MegaCorp, Inc.",
+            "--domain",
+            "example.com",
         ]
         ]
         # Run
         # Run
         coderedcms_main()
         coderedcms_main()
         # Assert files exist
         # 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()
         self.cleanup()
 
 
     def test_domain_www(self):
     def test_domain_www(self):
         self.setup()
         self.setup()
         # Set args
         # Set args
         sys.argv = [
         sys.argv = [
-            'coderedcms',
-            'start',
-            'myproject',
+            "coderedcms",
+            "start",
+            "myproject",
             self.TEST_DIR,
             self.TEST_DIR,
-            '--domain', 'www.example.com'
+            "--domain",
+            "www.example.com",
         ]
         ]
         # Run
         # Run
         coderedcms_main()
         coderedcms_main()
         # Assert files exist
         # 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()
         self.cleanup()
 
 
     def test_template_sass(self):
     def test_template_sass(self):
         self.setup()
         self.setup()
         # Set args
         # Set args
-        sys.argv = ['coderedcms', 'start', 'myproject', self.TEST_DIR, '--template', 'sass']
+        sys.argv = [
+            "coderedcms",
+            "start",
+            "myproject",
+            self.TEST_DIR,
+            "--template",
+            "sass",
+        ]
         # Run
         # Run
         coderedcms_main()
         coderedcms_main()
         # Assert files exist
         # 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()
         self.cleanup()

+ 7 - 6
coderedcms/tests/test_templates.py

@@ -12,12 +12,13 @@ EXPECTED_BANNER_HTML = """
 
 
 @pytest.mark.django_db
 @pytest.mark.django_db
 class TestSiteBanner(TestCase):
 class TestSiteBanner(TestCase):
-
     @override_settings(CRX_BANNER="Test")
     @override_settings(CRX_BANNER="Test")
     def test_with_banner(self):
     def test_with_banner(self):
         response = self.client.get("/")
         response = self.client.get("/")
         self.assertEqual(response.status_code, 200)
         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):
     def test_without_banner(self):
         response = self.client.get("/")
         response = self.client.get("/")
@@ -29,9 +30,7 @@ class TestSiteBanner(TestCase):
 class TestWagtailAdminBanner(TestCase):
 class TestWagtailAdminBanner(TestCase):
     def setUp(self):
     def setUp(self):
         admin = get_user_model().objects.create_superuser(
         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)
         self.client.force_login(admin)
 
 
@@ -42,7 +41,9 @@ class TestWagtailAdminBanner(TestCase):
     def test_with_banner(self):
     def test_with_banner(self):
         response = self.client.get("/admin/")
         response = self.client.get("/admin/")
         self.assertEqual(response.status_code, 200)
         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):
     def test_without_banner(self):
         response = self.client.get("/admin/")
         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
 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_:\.-]*$")
 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 wagtail.images.tests.utils import Image, get_test_image_file
 
 
 from coderedcms.models import LayoutSettings
 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
 @pytest.mark.django_db
@@ -31,34 +35,32 @@ class TestSiteURLs(unittest.TestCase):
         response = self.client.get("/sitemap.xml")
         response = self.client.get("/sitemap.xml")
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response['content-type'], 'application/xml')
+        self.assertEqual(response["content-type"], "application/xml")
 
 
     def test_robots(self):
     def test_robots(self):
         response = self.client.get("/robots.txt")
         response = self.client.get("/robots.txt")
 
 
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
-        self.assertEqual(response['content-type'], 'text/plain')
+        self.assertEqual(response["content-type"], "text/plain")
 
 
     def test_search(self):
     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.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.status_code, 200)
-        self.assertEqual(response.context['results'], None)
+        self.assertEqual(response.context["results"], None)
 
 
 
 
 @pytest.mark.django_db
 @pytest.mark.django_db
@@ -69,10 +71,10 @@ class TestEventURLs(unittest.TestCase):
 
 
     def test_generate_single_event(self):
     def test_generate_single_event(self):
         event_page = EventPage(
         event_page = EventPage(
-            path='/single-event/',
+            path="/single-event/",
             depth=1,
             depth=1,
-            title='Single Event',
-            slug='single-event'
+            title="Single Event",
+            slug="single-event",
         )
         )
         self.root_page.add_child(instance=event_page)
         self.root_page.add_child(instance=event_page)
         occurrence = EventOccurrence(
         occurrence = EventOccurrence(
@@ -87,34 +89,42 @@ class TestEventURLs(unittest.TestCase):
         response = self.client.post(
         response = self.client.post(
             ajax_url,
             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.status_code, 200)
-        self.assertEqual(response['Filename'], "{0}.ics".format(event_page.slug))
         self.assertEqual(
         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
         # Get datetimes from response and compare them to datetimes on page
         # startswith() is used because older versions of Python
         # startswith() is used because older versions of Python
         # use different datetime formatting, specifically for timezones
         # 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(
         self.assertTrue(
             start.startswith(
             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(
         self.assertTrue(
             end.startswith(
             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",
                 "event_pk": "junk",
                 "datetime_start": "junk",
                 "datetime_start": "junk",
                 "datetime_end": "junk",
                 "datetime_end": "junk",
-            }
+            },
         )
         )
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.status_code, 400)
         response = self.client.post(
         response = self.client.post(
@@ -138,54 +148,58 @@ class TestEventURLs(unittest.TestCase):
                 "event_pk": "junk",
                 "event_pk": "junk",
                 "datetime_start": "2022-07-14T10:00:00+0000",
                 "datetime_start": "2022-07-14T10:00:00+0000",
                 "datetime_end": "2022-07-14T10:00:00+0000",
                 "datetime_end": "2022-07-14T10:00:00+0000",
-            }
+            },
         )
         )
         self.assertEqual(response.status_code, 404)
         self.assertEqual(response.status_code, 404)
 
 
     def test_generate_recurring_event(self):
     def test_generate_recurring_event(self):
         event_page = EventPage(
         event_page = EventPage(
-            path='/recurring-event/',
+            path="/recurring-event/",
             depth=1,
             depth=1,
-            title='Recurring Event',
-            slug='recurring-event'
+            title="Recurring Event",
+            slug="recurring-event",
         )
         )
         self.root_page.add_child(instance=event_page)
         self.root_page.add_child(instance=event_page)
         occurrence = EventOccurrence(
         occurrence = EventOccurrence(
             event=event_page,
             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()
         occurrence.save()
 
 
         ajax_url = reverse("event_generate_recurring_ical")
         ajax_url = reverse("event_generate_recurring_ical")
 
 
         response = self.client.post(
         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.status_code, 200)
-        self.assertEqual(response['Filename'], "{0}.ics".format(event_page.slug))
         self.assertEqual(
         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
         # Get datetimes from response and compare them to datetimes on page
         # startswith() is used because older versions of Python
         # startswith() is used because older versions of Python
         # use different datetime formatting, specifically for timezones
         # 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(
         self.assertTrue(
             start.startswith(
             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(
         self.assertTrue(
             end.startswith(
             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):
     def test_generate_calendar(self):
         calendar_page = EventIndexPage(
         calendar_page = EventIndexPage(
-            path='/event-index-page/',
+            path="/event-index-page/",
             depth=1,
             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)
         self.root_page.add_child(instance=calendar_page)
 
 
         event_page = EventPage(
         event_page = EventPage(
-            path='/eventpage/1/',
+            path="/eventpage/1/",
             depth=2,
             depth=2,
-            title='Event Page 1',
-            slug='eventpage1'
+            title="Event Page 1",
+            slug="eventpage1",
         )
         )
         calendar_page.add_child(instance=event_page)
         calendar_page.add_child(instance=event_page)
         occurrence = EventOccurrence(
         occurrence = EventOccurrence(
             event=event_page,
             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()
         occurrence.save()
 
 
         ajax_url = reverse("event_generate_ical_for_calendar")
         ajax_url = reverse("event_generate_ical_for_calendar")
 
 
         response = self.client.post(
         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.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
         # Get datetimes from response and compare them to datetimes on page
         # startswith() is used because older versions of Python
         # startswith() is used because older versions of Python
         # use different datetime formatting, specifically for timezones
         # 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(
         self.assertTrue(
             start.startswith(
             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(
         self.assertTrue(
             end.startswith(
             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):
     def test_ajax_calendar(self):
         calendar_page = EventIndexPage(
         calendar_page = EventIndexPage(
-            path='/event-index-page/',
+            path="/event-index-page/",
             depth=1,
             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)
         self.root_page.add_child(instance=calendar_page)
 
 
         event_page = EventPage(
         event_page = EventPage(
-            path='/eventpage/1/',
+            path="/eventpage/1/",
             depth=2,
             depth=2,
-            title='Event Page 1',
-            slug='eventpage1'
+            title="Event Page 1",
+            slug="eventpage1",
         )
         )
         calendar_page.add_child(instance=event_page)
         calendar_page.add_child(instance=event_page)
         occurrence_one = EventOccurrence(
         occurrence_one = EventOccurrence(
             event=event_page,
             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()
         occurrence_one.save()
 
 
@@ -281,13 +299,13 @@ class TestEventURLs(unittest.TestCase):
         response = self.client.post(
         response = self.client.post(
             f"{ajax_url}?pid={calendar_page.pk}",
             f"{ajax_url}?pid={calendar_page.pk}",
             follow=True,
             follow=True,
-            **{'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
+            **{"HTTP_X_REQUESTED_WITH": "XMLHttpRequest"},
         )
         )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.status_code, 200)
 
 
         # Get datetimes from response and compare them to datetimes on page
         # 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(
         event_local_start = timezone.localtime(
             EventOccurrence.objects.get(event=event_page).start
             EventOccurrence.objects.get(event=event_page).start
         )
         )
@@ -295,13 +313,9 @@ class TestEventURLs(unittest.TestCase):
             EventOccurrence.objects.get(event=event_page).end
             EventOccurrence.objects.get(event=event_page).end
         )
         )
         self.assertEqual(
         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.
         # Test that garbage requests are handled appropriately.
         response = self.client.post(ajax_url)
         response = self.client.post(ajax_url)

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

@@ -12,7 +12,7 @@ from coderedcms.models import (
     CoderedLocationIndexPage,
     CoderedLocationIndexPage,
     CoderedLocationPage,
     CoderedLocationPage,
     CoderedStreamFormPage,
     CoderedStreamFormPage,
-    CoderedWebPage
+    CoderedWebPage,
 )
 )
 
 
 
 
@@ -20,111 +20,122 @@ class ArticlePage(CoderedArticlePage):
     """
     """
     Article, suitable for news or blog content.
     Article, suitable for news or blog content.
     """
     """
+
     class Meta:
     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.
     # 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):
 class ArticleIndexPage(CoderedArticleIndexPage):
     """
     """
     Shows a list of article sub-pages.
     Shows a list of article sub-pages.
     """
     """
+
     class Meta:
     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.
     # Override to specify custom index ordering choice/default.
-    index_query_pagemodel = 'testapp.ArticlePage'
+    index_query_pagemodel = "testapp.ArticlePage"
 
 
     # Only allow ArticlePages beneath this page.
     # 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):
 class FormPage(CoderedFormPage):
     """
     """
     A page with an html <form>.
     A page with an html <form>.
     """
     """
+
     class Meta:
     class Meta:
-        verbose_name = 'Form'
+        verbose_name = "Form"
 
 
-    template = 'coderedcms/pages/form_page.html'
+    template = "coderedcms/pages/form_page.html"
 
 
 
 
 class FormPageField(CoderedFormField):
 class FormPageField(CoderedFormField):
     """
     """
     A field that links to a FormPage.
     A field that links to a FormPage.
     """
     """
+
     class Meta:
     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):
 class FormConfirmEmail(CoderedEmail):
     """
     """
     Sends a confirmation email after submitting a FormPage.
     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):
 class WebPage(CoderedWebPage):
     """
     """
     General use page with featureful streamfield and SEO attributes.
     General use page with featureful streamfield and SEO attributes.
     """
     """
+
     class Meta:
     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 EventPage(CoderedEventPage):
     class Meta:
     class Meta:
-        verbose_name = 'Event Page'
+        verbose_name = "Event Page"
 
 
-    parent_page_types = ['testapp.EventIndexPage']
+    parent_page_types = ["testapp.EventIndexPage"]
     subpage_types = []
     subpage_types = []
-    template = 'coderedcms/pages/event_page.html'
+    template = "coderedcms/pages/event_page.html"
 
 
 
 
 class EventIndexPage(CoderedEventIndexPage):
 class EventIndexPage(CoderedEventIndexPage):
     """
     """
     Shows a list of event sub-pages.
     Shows a list of event sub-pages.
     """
     """
+
     class Meta:
     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.
     # 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):
 class EventOccurrence(CoderedEventOccurrence):
-    event = ParentalKey(EventPage, related_name='occurrences')
+    event = ParentalKey(EventPage, related_name="occurrences")
 
 
 
 
 class LocationPage(CoderedLocationPage):
 class LocationPage(CoderedLocationPage):
     """
     """
     A page that holds a location.  This could be a store, a restaurant, etc.
     A page that holds a location.  This could be a store, a restaurant, etc.
     """
     """
+
     class Meta:
     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.
     # Only allow LocationIndexPages above this page.
-    parent_page_types = ['testapp.LocationIndexPage']
+    parent_page_types = ["testapp.LocationIndexPage"]
 
 
 
 
 class LocationIndexPage(CoderedLocationIndexPage):
 class LocationIndexPage(CoderedLocationIndexPage):
@@ -133,27 +144,28 @@ class LocationIndexPage(CoderedLocationIndexPage):
     This does require a Google Maps API Key that can be defined in Settings >
     This does require a Google Maps API Key that can be defined in Settings >
     Google API Settings
     Google API Settings
     """
     """
+
     class Meta:
     class Meta:
-        verbose_name = 'Location Landing Page'
+        verbose_name = "Location Landing Page"
 
 
     # Override to specify custom index ordering choice/default.
     # Override to specify custom index ordering choice/default.
-    index_query_pagemodel = 'testapp.LocationPage'
+    index_query_pagemodel = "testapp.LocationPage"
 
 
     # Only allow LocationPages beneath this page.
     # 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 StreamFormPage(CoderedStreamFormPage):
     class Meta:
     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):
 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).
     Tests indexing features (show/sort/filter child pages).
     """
     """
+
     class Meta:
     class Meta:
         verbose_name = "Index Test Page"
         verbose_name = "Index Test Page"
 
 
     index_query_pagemodel = "testapp.WebPage"
     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 = [
 urlpatterns = [
     # Admin
     # Admin
-    path('django-admin/', admin.site.urls),
-    path('admin/', include(coderedadmin_urls)),
-
+    path("django-admin/", admin.site.urls),
+    path("admin/", include(coderedadmin_urls)),
     # Documents
     # Documents
-    path('docs/', include(wagtaildocs_urls)),
-
+    path("docs/", include(wagtaildocs_urls)),
     # Search
     # Search
-    path('search/', include(coderedsearch_urls)),
-
+    path("search/", include(coderedsearch_urls)),
     # For anything not caught by a more specific rule above, hand over to
     # 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 page serving mechanism. This should be the last pattern in
     # the list:
     # 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
     # Alternatively, if you want CMS pages to be served from a subpath
     # of your site, rather than the site root:
     # of your site, rather than the site root:
     #    re_path(r'^pages/', include(codered_urls)),
     #    re_path(r'^pages/', include(codered_urls)),

+ 30 - 18
coderedcms/urls.py

@@ -9,31 +9,43 @@ from coderedcms.views import (
     event_get_calendar_events,
     event_get_calendar_events,
     favicon,
     favicon,
     robots,
     robots,
-    serve_protected_file
+    serve_protected_file,
 )
 )
 
 
 
 
 urlpatterns = [
 urlpatterns = [
     # CodeRed custom URLs
     # 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,
         serve_protected_file,
-        name="serve_protected_file"
+        name="serve_protected_file",
     ),
     ),
-
     # Event/Calendar URLs
     # 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
     # 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:
     if render_link:
         return mark_safe(
         return mark_safe(
             "<a href='{0}{1}'>{0}{1}</a>".format(
             "<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):
 def uri_validator(possible_uri):

+ 94 - 64
coderedcms/views.py

@@ -1,10 +1,20 @@
 import mimetypes
 import mimetypes
 import os
 import os
 from datetime import datetime
 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.auth.decorators import login_required
 from django.contrib.contenttypes.models import ContentType
 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.shortcuts import redirect, render
 from django.utils import timezone
 from django.utils import timezone
 from django.utils.translation import ngettext, gettext_lazy as _
 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 wagtail.models import Page, get_page_models
 from coderedcms import utils
 from coderedcms import utils
 from coderedcms.forms import SearchForm
 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.settings import crx_settings
 from coderedcms.templatetags.coderedcms_tags import get_name_of_class
 from coderedcms.templatetags.coderedcms_tags import get_name_of_class
 
 
@@ -34,8 +44,8 @@ def search(request):
     results_paginated = None
     results_paginated = None
 
 
     if search_form.is_valid():
     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
         # get all page models
         pagemodels = sorted(get_page_models(), key=get_name_of_class)
         pagemodels = sorted(get_page_models(), key=get_name_of_class)
@@ -48,7 +58,9 @@ def search(request):
         if search_model:
         if search_model:
             try:
             try:
                 # If provided a model name, try to get it
                 # 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)
                 results = results.type(model)
             except ContentType.DoesNotExist:
             except ContentType.DoesNotExist:
                 # Maintain existing behavior of only returning objects if the page type is real
                 # Maintain existing behavior of only returning objects if the page type is real
@@ -57,8 +69,10 @@ def search(request):
         # get and paginate results
         # get and paginate results
         if results:
         if results:
             results = results.search(search_query)
             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:
             try:
                 results_paginated = paginator.page(page)
                 results_paginated = paginator.page(page)
             except PageNotAnInteger:
             except PageNotAnInteger:
@@ -69,13 +83,17 @@ def search(request):
                 results_paginated = paginator.page(1)
                 results_paginated = paginator.page(1)
 
 
     # Render template
     # 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
 @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.
     # Path must be a sub-path of the PROTECTED_MEDIA_ROOT, and exist.
     if fullpath.startswith(mediapath) and os.path.isfile(fullpath):
     if fullpath.startswith(mediapath) and os.path.isfile(fullpath):
         mimetype, encoding = mimetypes.guess_type(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)
             response = HttpResponse(f.read(), content_type=mimetype)
         if encoding:
         if encoding:
             response["Content-Encoding"] = encoding
             response["Content-Encoding"] = encoding
@@ -102,29 +120,29 @@ def serve_protected_file(request, path):
 def favicon(request):
 def favicon(request):
     icon = LayoutSettings.for_request(request).favicon
     icon = LayoutSettings.for_request(request).favicon
     if icon:
     if icon:
-        return HttpResponsePermanentRedirect(icon.get_rendition('original').url)
+        return HttpResponsePermanentRedirect(icon.get_rendition("original").url)
     raise Http404()
     raise Http404()
 
 
 
 
 def robots(request):
 def robots(request):
-    return render(
-        request,
-        'robots.txt',
-        content_type='text/plain'
-    )
+    return render(request, "robots.txt", content_type="text/plain")
 
 
 
 
 @require_POST
 @require_POST
 def event_generate_single_ical_for_event(request):
 def event_generate_single_ical_for_event(request):
     # Parse input.
     # Parse input.
     try:
     try:
-        event_pk = request.POST['event_pk']
+        event_pk = request.POST["event_pk"]
     except KeyError:
     except KeyError:
         return HttpResponse("event_pk required", status=400)
         return HttpResponse("event_pk required", status=400)
 
 
     try:
     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_start = None
         dt_end = None
         dt_end = None
         if dt_start_str:
         if dt_start_str:
@@ -150,12 +168,16 @@ def event_generate_single_ical_for_event(request):
 
 
     # Generate the ical file.
     # Generate the ical file.
     ical = Calendar()
     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 = 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
     return response
 
 
 
 
@@ -163,7 +185,7 @@ def event_generate_single_ical_for_event(request):
 def event_generate_recurring_ical_for_event(request):
 def event_generate_recurring_ical_for_event(request):
     # Parse input.
     # Parse input.
     try:
     try:
-        event_pk = request.POST['event_pk']
+        event_pk = request.POST["event_pk"]
     except KeyError:
     except KeyError:
         return HttpResponse("event_pk required", status=400)
         return HttpResponse("event_pk required", status=400)
 
 
@@ -175,13 +197,15 @@ def event_generate_recurring_ical_for_event(request):
 
 
     # Generate the ical file.
     # Generate the ical file.
     ical = Calendar()
     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():
     for e in event.create_recurring_ical():
         ical.add_component(e)
         ical.add_component(e)
     response = HttpResponse(ical.to_ical(), content_type="text/calendar")
     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
     return response
 
 
 
 
@@ -201,14 +225,14 @@ def event_generate_ical_for_calendar(request):
 
 
     # Generate the ical file.
     # Generate the ical file.
     ical = Calendar()
     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 event_page in page.get_index_children():
         for e in event_page.specific.create_recurring_ical():
         for e in event_page.specific.create_recurring_ical():
             ical.add_component(e)
             ical.add_component(e)
     response = HttpResponse(ical.to_ical(), content_type="text/calendar")
     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
     return response
 
 
 
 
@@ -224,8 +248,8 @@ def event_get_calendar_events(request):
 
 
     start = None
     start = None
     end = 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:
     try:
         if start_str:
         if start_str:
             start = timezone.make_aware(
             start = timezone.make_aware(
@@ -237,8 +261,7 @@ def event_get_calendar_events(request):
             )
             )
     except ValueError:
     except ValueError:
         return HttpResponse(
         return HttpResponse(
-            "start and end must be valid datetimes.",
-            status=400
+            "start and end must be valid datetimes.", status=400
         )
         )
 
 
     # Get the page.
     # Get the page.
@@ -248,8 +271,7 @@ def event_get_calendar_events(request):
         raise Http404("Page does not exist")
         raise Http404("Page does not exist")
 
 
     return JsonResponse(
     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.
     Landing page to replace wagtailimportexport.
     """
     """
-    return render(request, 'wagtailimportexport/index.html')
+    return render(request, "wagtailimportexport/index.html")
 
 
 
 
 @login_required
 @login_required
@@ -269,30 +291,38 @@ def import_pages_from_csv_file(request):
     format that the importer expects.
     format that the importer expects.
     """
     """
 
 
-    if request.method == 'POST':
+    if request.method == "POST":
         form = ImportPagesFromCSVFileForm(request.POST, request.FILES)
         form = ImportPagesFromCSVFileForm(request.POST, request.FILES)
         if form.is_valid():
         if form.is_valid():
             import_data = convert_csv_to_json(
             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:
             try:
                 page_count = import_pages(import_data, parent_page)
                 page_count = import_pages(import_data, parent_page)
             except LookupError as e:
             except LookupError as e:
-                messages.error(request, _(
-                    "Import failed: %(reason)s") % {'reason': e}
+                messages.error(
+                    request, _("Import failed: %(reason)s") % {"reason": e}
                 )
                 )
             else:
             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:
     else:
         form = ImportPagesFromCSVFileForm()
         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 django.utils.translation import gettext_lazy as _
 from anyascii import anyascii
 from anyascii import anyascii
 from wagtail.blocks import (
 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):
 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
     field_class = forms.CharField
     widget = None
     widget = None
 
 
     def get_slug(self, struct_value):
     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):
     def get_field_class(self, struct_value):
         return self.field_class
         return self.field_class
@@ -27,71 +36,76 @@ class FormFieldBlock(StructBlock):
         return self.widget
         return self.widget
 
 
     def get_field_kwargs(self, struct_value):
     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)
         form_widget = self.get_widget(struct_value)
         if form_widget is not None:
         if form_widget is not None:
-            kwargs['widget'] = form_widget
+            kwargs["widget"] = form_widget
         return kwargs
         return kwargs
 
 
     def get_field(self, struct_value):
     def get_field(self, struct_value):
         return self.get_field_class(struct_value)(
         return self.get_field_class(struct_value)(
-            **self.get_field_kwargs(struct_value))
+            **self.get_field_kwargs(struct_value)
+        )
 
 
 
 
 class OptionalFormFieldBlock(FormFieldBlock):
 class OptionalFormFieldBlock(FormFieldBlock):
-    required = BooleanBlock(label=_('Required'), required=False)
+    required = BooleanBlock(label=_("Required"), required=False)
 
 
 
 
 CHARFIELD_FORMATS = [
 CHARFIELD_FORMATS = [
-    ('email', _('Email')),
-    ('url', _('URL')),
+    ("email", _("Email")),
+    ("url", _("URL")),
 ]
 ]
 try:
 try:
     from phonenumber_field.formfields import PhoneNumberField
     from phonenumber_field.formfields import PhoneNumberField
 except ImportError:
 except ImportError:
     pass
     pass
 else:
 else:
-    CHARFIELD_FORMATS.append(('phone', _('Phone')))
+    CHARFIELD_FORMATS.append(("phone", _("Phone")))
 
 
 
 
 class CharFieldBlock(OptionalFormFieldBlock):
 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:
     class Meta:
-        label = _('Text field (single line)')
+        label = _("Text field (single line)")
 
 
     def get_field_class(self, struct_value):
     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
             return forms.URLField
-        if text_format == 'email':
+        if text_format == "email":
             return forms.EmailField
             return forms.EmailField
-        if text_format == 'phone':
+        if text_format == "phone":
             return PhoneNumberField
             return PhoneNumberField
         return super().get_field_class(struct_value)
         return super().get_field_class(struct_value)
 
 
 
 
 class TextFieldBlock(OptionalFormFieldBlock):
 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:
     class Meta:
-        label = _('Text field (multi line)')
+        label = _("Text field (multi line)")
 
 
 
 
 class NumberFieldBlock(OptionalFormFieldBlock):
 class NumberFieldBlock(OptionalFormFieldBlock):
-    default_value = CharBlock(required=False, label=_('Default value'))
+    default_value = CharBlock(required=False, label=_("Default value"))
 
 
     widget = forms.NumberInput
     widget = forms.NumberInput
 
 
     class Meta:
     class Meta:
-        label = _('Number field')
+        label = _("Number field")
 
 
 
 
 class CheckboxFieldBlock(FormFieldBlock):
 class CheckboxFieldBlock(FormFieldBlock):
@@ -100,24 +114,25 @@ class CheckboxFieldBlock(FormFieldBlock):
     field_class = forms.BooleanField
     field_class = forms.BooleanField
 
 
     class Meta:
     class Meta:
-        label = _('Checkbox field')
-        icon = 'tick-inverse'
+        label = _("Checkbox field")
+        icon = "tick-inverse"
 
 
 
 
 class RadioButtonsFieldBlock(OptionalFormFieldBlock):
 class RadioButtonsFieldBlock(OptionalFormFieldBlock):
-    choices = ListBlock(CharBlock(label=_('Choice')))
+    choices = ListBlock(CharBlock(label=_("Choice")))
 
 
     field_class = forms.ChoiceField
     field_class = forms.ChoiceField
     widget = forms.RadioSelect
     widget = forms.RadioSelect
 
 
     class Meta:
     class Meta:
-        label = _('Radio buttons')
-        icon = 'radio-empty'
+        label = _("Radio buttons")
+        icon = "radio-empty"
 
 
     def get_field_kwargs(self, struct_value):
     def get_field_kwargs(self, struct_value):
         kwargs = super().get_field_kwargs(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
         return kwargs
 
 
 
 
@@ -125,44 +140,47 @@ class DropdownFieldBlock(RadioButtonsFieldBlock):
     widget = forms.Select
     widget = forms.Select
 
 
     class Meta:
     class Meta:
-        label = _('Dropdown field')
-        icon = 'arrow-down-big'
+        label = _("Dropdown field")
+        icon = "arrow-down-big"
 
 
     def get_field_kwargs(self, struct_value):
     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
         return kwargs
 
 
 
 
 class CheckboxesFieldBlock(OptionalFormFieldBlock):
 class CheckboxesFieldBlock(OptionalFormFieldBlock):
-    checkboxes = ListBlock(CharBlock(label=_('Checkbox')))
+    checkboxes = ListBlock(CharBlock(label=_("Checkbox")))
 
 
     field_class = forms.MultipleChoiceField
     field_class = forms.MultipleChoiceField
     widget = forms.CheckboxSelectMultiple
     widget = forms.CheckboxSelectMultiple
 
 
     class Meta:
     class Meta:
-        label = _('Multiple checkboxes field')
-        icon = 'list-ul'
+        label = _("Multiple checkboxes field")
+        icon = "list-ul"
 
 
     def get_field_kwargs(self, struct_value):
     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
         return kwargs
 
 
 
 
 class DatePickerInput(forms.DateInput):
 class DatePickerInput(forms.DateInput):
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
-        attrs = kwargs.get('attrs')
+        attrs = kwargs.get("attrs")
         if attrs is None:
         if attrs is None:
             attrs = {}
             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)
         super().__init__(*args, **kwargs)
 
 
 
 
@@ -173,12 +191,12 @@ class DateFieldBlock(OptionalFormFieldBlock):
     widget = DatePickerInput
     widget = DatePickerInput
 
 
     class Meta:
     class Meta:
-        label = _('Date field')
-        icon = 'date'
+        label = _("Date field")
+        icon = "date"
 
 
 
 
 class HTML5TimeInput(forms.TimeInput):
 class HTML5TimeInput(forms.TimeInput):
-    input_type = 'time'
+    input_type = "time"
 
 
 
 
 class TimeFieldBlock(OptionalFormFieldBlock):
 class TimeFieldBlock(OptionalFormFieldBlock):
@@ -188,14 +206,15 @@ class TimeFieldBlock(OptionalFormFieldBlock):
     widget = HTML5TimeInput
     widget = HTML5TimeInput
 
 
     class Meta:
     class Meta:
-        label = _('Time field')
-        icon = 'time'
+        label = _("Time field")
+        icon = "time"
 
 
 
 
 class DateTimePickerInput(forms.SplitDateTimeWidget):
 class DateTimePickerInput(forms.SplitDateTimeWidget):
     def __init__(self, attrs=None, date_format=None, time_format=None):
     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 = (
         self.widgets = (
             DatePickerInput(attrs=attrs, format=date_format),
             DatePickerInput(attrs=attrs, format=date_format),
             HTML5TimeInput(attrs=attrs, format=time_format),
             HTML5TimeInput(attrs=attrs, format=time_format),
@@ -214,55 +233,55 @@ class DateTimeFieldBlock(OptionalFormFieldBlock):
     widget = DateTimePickerInput
     widget = DateTimePickerInput
 
 
     class Meta:
     class Meta:
-        label = _('Date+time field')
-        icon = 'date'
+        label = _("Date+time field")
+        icon = "date"
 
 
 
 
 class ImageFieldBlock(OptionalFormFieldBlock):
 class ImageFieldBlock(OptionalFormFieldBlock):
     field_class = forms.ImageField
     field_class = forms.ImageField
 
 
     class Meta:
     class Meta:
-        label = _('Image field')
-        icon = 'image'
+        label = _("Image field")
+        icon = "image"
 
 
 
 
 class FileFieldBlock(OptionalFormFieldBlock):
 class FileFieldBlock(OptionalFormFieldBlock):
     field_class = forms.FileField
     field_class = forms.FileField
 
 
     class Meta:
     class Meta:
-        label = _('File field')
-        icon = 'download'
+        label = _("File field")
+        icon = "download"
 
 
 
 
 class FormFieldsBlock(StreamBlock):
 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:
     class Meta:
-        label = _('Form fields')
+        label = _("Form fields")
 
 
 
 
 class FormStepBlock(StructBlock):
 class FormStepBlock(StructBlock):
-    name = CharBlock(label=_('Name'), required=False)
+    name = CharBlock(label=_("Name"), required=False)
     form_fields = FormFieldsBlock()
     form_fields = FormFieldsBlock()
 
 
     class Meta:
     class Meta:
-        label = _('Form step')
+        label = _("Form step")
 
 
 
 
 class FormStepsBlock(StreamBlock):
 class FormStepsBlock(StreamBlock):
     step = FormStepBlock()
     step = FormStepBlock()
 
 
     class Meta:
     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):
     def bind_to(self, model=None, instance=None, request=None, form=None):
         new = super().bind_to(model=model)
         new = super().bind_to(model=model)
         if self.heading is None:
         if self.heading is None:
-            new.heading = _('{} submissions').format(model.get_verbose_name())
+            new.heading = _("{} submissions").format(model.get_verbose_name())
         return new
         return new
 
 
     def render(self):
     def render(self):
@@ -20,11 +20,17 @@ class FormSubmissionsPanel(EditHandler):
         submission_count = submissions.count()
         submission_count = submissions.count()
 
 
         if not submission_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.files.storage import default_storage
 from django.core.serializers.json import DjangoJSONEncoder
 from django.core.serializers.json import DjangoJSONEncoder
 from django.db.models import (
 from django.db.models import (
-    CharField, TextField, DateTimeField, Model, ForeignKey, PROTECT, CASCADE,
+    CharField,
+    TextField,
+    DateTimeField,
+    Model,
+    ForeignKey,
+    PROTECT,
+    CASCADE,
     QuerySet,
     QuerySet,
 )
 )
 from django.db.models.fields.files import FieldFile
 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 django.utils.translation import gettext_lazy as _
 from wagtail.models import Page
 from wagtail.models import Page
 from wagtail.contrib.forms.models import (
 from wagtail.contrib.forms.models import (
-    AbstractForm, AbstractEmailForm, AbstractFormSubmission)
+    AbstractForm,
+    AbstractEmailForm,
+    AbstractFormSubmission,
+)
 
 
 from .blocks import FormStepBlock, FormFieldBlock
 from .blocks import FormStepBlock, FormFieldBlock
 
 
@@ -38,14 +47,14 @@ class Step:
     def __init__(self, steps, index, struct_child):
     def __init__(self, steps, index, struct_child):
         self.steps = steps
         self.steps = steps
         self.index = index
         self.index = index
-        block = getattr(struct_child, 'block', None)
+        block = getattr(struct_child, "block", None)
         if block is None:
         if block is None:
             struct_child = []
             struct_child = []
         if isinstance(block, FormStepBlock):
         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:
         else:
-            self.name = ''
+            self.name = ""
             self.form_fields = struct_child
             self.form_fields = struct_child
 
 
     @property
     @property
@@ -54,7 +63,7 @@ class Step:
 
 
     @property
     @property
     def url(self):
     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):
     def get_form_fields(self):
         form_fields = OrderedDict()
         form_fields = OrderedDict()
@@ -68,8 +77,11 @@ class Step:
         return form_fields
         return form_fields
 
 
     def get_form_class(self):
     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):
     def get_markups_and_bound_fields(self, form):
         for struct_child in self.form_fields:
         for struct_child in self.form_fields:
@@ -77,22 +89,24 @@ class Step:
             if isinstance(block, FormFieldBlock):
             if isinstance(block, FormFieldBlock):
                 struct_value = struct_child.value
                 struct_value = struct_child.value
                 field_name = block.get_slug(struct_value)
                 field_name = block.get_slug(struct_value)
-                yield form[field_name], 'field'
+                yield form[field_name], "field"
             else:
             else:
-                yield mark_safe(struct_child), 'markup'
+                yield mark_safe(struct_child), "markup"
 
 
     def __str__(self):
     def __str__(self):
         if self.name:
         if self.name:
             return self.name
             return self.name
-        return _('Step %s') % self.index1
+        return _("Step %s") % self.index1
 
 
     @property
     @property
     def badge(self):
     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):
     def __html__(self):
-        return '%s %s' % (self, self.badge)
+        return "%s %s" % (self, self.badge)
 
 
     @property
     @property
     def is_active(self):
     def is_active(self):
@@ -113,23 +127,25 @@ class Step:
     @property
     @property
     def prev(self):
     def prev(self):
         if self.has_prev:
         if self.has_prev:
-            return self.steps[self.index-1]
+            return self.steps[self.index - 1]
 
 
     @property
     @property
     def next(self):
     def next(self):
         if self.has_next:
         if self.has_next:
-            return self.steps[self.index+1]
+            return self.steps[self.index + 1]
 
 
     def get_existing_data(self, raw=False):
     def get_existing_data(self, raw=False):
         data = self.steps.get_existing_data()[self.index]
         data = self.steps.get_existing_data()[self.index]
         fields = self.get_form_fields()
         fields = self.get_form_fields()
         if not raw:
         if not raw:
+
             class FakeField:
             class FakeField:
                 storage = self.steps.get_storage()
                 storage = self.steps.get_storage()
 
 
             for field_name, value in data.items():
             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)
                     data[field_name] = FieldFile(None, FakeField, value)
         return data
         return data
 
 
@@ -157,11 +173,15 @@ class Steps(list):
         # TODO: Make it possible to change the `form_fields` attribute.
         # TODO: Make it possible to change the `form_fields` attribute.
         self.form_fields = page.form_fields
         self.form_fields = page.form_fields
         self.request = request
         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:
         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:
         else:
             steps = [Step(self, 0, self.form_fields)]
             steps = [Step(self, 0, self.form_fields)]
         super().__init__(steps)
         super().__init__(steps)
@@ -186,9 +206,10 @@ class Steps(list):
     @current.setter
     @current.setter
     def current(self, new_index: int):
     def current(self, new_index: int):
         if not isinstance(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):
     def forward(self, increment: int = 1):
         self.current = self.current_index + increment
         self.current = self.current_index + increment
@@ -209,16 +230,19 @@ class Steps(list):
 
 
     def get_current_form(self):
     def get_current_form(self):
         request = self.request
         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()
                 self.backward()
             else:
             else:
                 return self.current.get_form_class()(
                 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()(
         return self.current.get_form_class()(
-            initial=self.current.get_existing_data())
+            initial=self.current.get_existing_data()
+        )
 
 
     def get_storage(self):
     def get_storage(self):
         return self.page.get_storage()
         return self.page.get_storage()
@@ -228,21 +252,21 @@ class Steps(list):
         for name, field in form.fields.items():
         for name, field in form.fields.items():
             if isinstance(field, FileField):
             if isinstance(field, FileField):
                 file = form.cleaned_data[name]
                 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
                     form.cleaned_data[name] = file.name
                     continue
                     continue
                 if submission is not None:
                 if submission is not None:
                     submission.delete_file(name)
                     submission.delete_file(name)
                 if not file:  # 'Clear' was checked.
                 if not file:  # 'Clear' was checked.
-                    form.cleaned_data[name] = ''
+                    form.cleaned_data[name] = ""
                     continue
                     continue
                 directory = self.request.session.session_key
                 directory = self.request.session.session_key
                 storage = self.get_storage()
                 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(
                 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():
                     for chunk in file.chunks():
                         destination.write(chunk)
                         destination.write(chunk)
                 form.cleaned_data[name] = path
                 form.cleaned_data[name] = path
@@ -271,29 +295,33 @@ class Steps(list):
 class SessionFormSubmission(AbstractFormSubmission):
 class SessionFormSubmission(AbstractFormSubmission):
 
 
     session_key = CharField(max_length=40, null=True, default=None)
     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({}))
     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 = (
     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)
     status = CharField(max_length=10, choices=STATUSES, default=INCOMPLETE)
 
 
     class Meta:
     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
         abstract = True
 
 
     @property
     @property
@@ -306,7 +334,8 @@ class SessionFormSubmission(AbstractFormSubmission):
 
 
     def get_session(self):
     def get_session(self):
         return import_module(settings.SESSION_ENGINE).SessionStore(
         return import_module(settings.SESSION_ENGINE).SessionStore(
-            session_key=self.session_key)
+            session_key=self.session_key
+        )
 
 
     def reset_step(self):
     def reset_step(self):
         session = self.get_session()
         session = self.get_session()
@@ -322,8 +351,8 @@ class SessionFormSubmission(AbstractFormSubmission):
 
 
     def get_thumbnail_path(self, path, width=64, height=64):
     def get_thumbnail_path(self, path, width=64, height=64):
         if not path:
         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_by_path = json.loads(self.thumbnails_by_path)
         thumbnails_paths = thumbnails_by_path.get(path)
         thumbnails_paths = thumbnails_by_path.get(path)
         if thumbnails_paths is None:
         if thumbnails_paths is None:
@@ -334,8 +363,7 @@ class SessionFormSubmission(AbstractFormSubmission):
                 return thumbnail_path
                 return thumbnail_path
 
 
         path = Path(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()
         storage = self.get_storage()
         thumbnail_path = storage.get_available_name(thumbnail_path)
         thumbnail_path = storage.get_available_name(thumbnail_path)
 
 
@@ -344,8 +372,9 @@ class SessionFormSubmission(AbstractFormSubmission):
         thumbnail.save(storage.path(thumbnail_path))
         thumbnail.save(storage.path(thumbnail_path))
 
 
         thumbnails_by_path[str(path)][variant] = 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()
         self.save()
         return thumbnail_path
         return thumbnail_path
 
 
@@ -365,7 +394,8 @@ class SessionFormSubmission(AbstractFormSubmission):
                 path = data.get(name)
                 path = data.get(name)
                 if path:
                 if path:
                     files[name] = [path] + list(
                     files[name] = [path] + list(
-                        self.get_existing_thumbnails(path))
+                        self.get_existing_thumbnails(path)
+                    )
         return files
         return files
 
 
     def get_all_files(self):
     def get_all_files(self):
@@ -379,43 +409,44 @@ class SessionFormSubmission(AbstractFormSubmission):
             self.get_storage().delete(path)
             self.get_storage().delete(path)
             if path in thumbnails_by_path:
             if path in thumbnails_by_path:
                 del thumbnails_by_path[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()
         self.save()
 
 
     def render_email(self, value):
     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):
     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):
     def render_image(self, value):
         storage = self.get_storage()
         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):
     def render_file(self, value):
         return mark_safe('<a href="%s" target="_blank">%s</a>') % (
         return mark_safe('<a href="%s" target="_blank">%s</a>') % (
             self.get_storage().url(value),
             self.get_storage().url(value),
-            Path(value).name
+            Path(value).name,
         )
         )
 
 
     def format_value(self, field, value):
     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)
         new_value = self.form_page.format_value(field, value)
         if new_value != value:
         if new_value != value:
             return new_value
             return new_value
         if value is True:
         if value is True:
-            return 'Yes'
+            return "Yes"
         if value is False:
         if value is False:
-            return 'No'
+            return "No"
         if isinstance(value, (list, tuple)):
         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):
         if isinstance(value, datetime.date):
             return value
             return value
         if isinstance(field, EmailField):
         if isinstance(field, EmailField):
@@ -426,30 +457,37 @@ class SessionFormSubmission(AbstractFormSubmission):
             return self.render_image(value)
             return self.render_image(value)
         if isinstance(field, FileField):
         if isinstance(field, FileField):
             return self.render_file(value)
             return self.render_file(value)
-        if isinstance(value, SafeData) or hasattr(value, '__html__'):
+        if isinstance(value, SafeData) or hasattr(value, "__html__"):
             return value
             return value
         return str(value)
         return str(value)
 
 
     def format_db_field(self, field_name, raw=False):
     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:
         if method is not None:
             return method()
             return method()
         value = getattr(self, field_name)
         value = getattr(self, field_name)
         if raw:
         if raw:
             return value
             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):
     def get_steps_data(self, raw=False):
         steps_data = json.loads(self.form_data)
         steps_data = json.loads(self.form_data)
         if raw:
         if raw:
             return steps_data
             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 [
         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):
     def get_extra_data(self, raw=False):
         return self.form_page.get_extra_data(self, raw=raw)
         return self.form_page.get_extra_data(self, raw=raw)
@@ -462,26 +500,30 @@ class SessionFormSubmission(AbstractFormSubmission):
             form_data.update(step_data)
             form_data.update(step_data)
         if add_metadata:
         if add_metadata:
             form_data.update(
             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
         return form_data
 
 
     def steps_with_data_iterator(self, raw=False):
     def steps_with_data_iterator(self, raw=False):
         for step, step_data_fields, step_data in zip(
         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)
 @receiver(post_delete, sender=SessionFormSubmission)
 def delete_files(sender, **kwargs):
 def delete_files(sender, **kwargs):
-    instance = kwargs['instance']
+    instance = kwargs["instance"]
     instance.reset_step()
     instance.reset_step()
     storage = instance.get_storage()
     storage = instance.get_storage()
     for path in instance.get_all_files():
     for path in instance.get_all_files():
@@ -514,34 +556,35 @@ class SubmissionRevisionQuerySet(QuerySet):
 
 
 
 
 class SubmissionRevision(Model):
 class SubmissionRevision(Model):
-    CREATED = 'created'
-    CHANGED = 'changed'
-    DELETED = 'deleted'
+    CREATED = "created"
+    CHANGED = "changed"
+    DELETED = "deleted"
     TYPES = (
     TYPES = (
-        (CREATED, _('Created')),
-        (CHANGED, _('Changed')),
-        (DELETED, _('Deleted')),
+        (CREATED, _("Created")),
+        (CHANGED, _("Changed")),
+        (DELETED, _("Deleted")),
     )
     )
     type = CharField(max_length=7, choices=TYPES)
     type = CharField(max_length=7, choices=TYPES)
     created_at = DateTimeField(auto_now_add=True)
     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_id = TextField()
-    submission = GenericForeignKey('submission_ct', 'submission_id')
+    submission = GenericForeignKey("submission_ct", "submission_id")
     data = TextField()
     data = TextField()
     summary = TextField()
     summary = TextField()
 
 
     objects = SubmissionRevisionQuerySet.as_manager()
     objects = SubmissionRevisionQuerySet.as_manager()
 
 
     class Meta:
     class Meta:
-        ordering = ('-created_at',)
+        ordering = ("-created_at",)
         abstract = True
         abstract = True
 
 
     @staticmethod
     @staticmethod
     def get_filters_for(submission):
     def get_filters_for(submission):
         return {
         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
     @classmethod
@@ -554,56 +597,72 @@ class SubmissionRevision(Model):
             value2 = data2.get(k)
             value2 = data2.get(k)
             if value2 == value1 or not value1 and not value2:
             if value2 == value1 or not value1 and not value2:
                 continue
                 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.
             # Escapes newlines as they are used as separator inside summaries.
             if isinstance(value1, str):
             if isinstance(value1, str):
-                value1 = value1.replace('\n', r'\n')
+                value1 = value1.replace("\n", r"\n")
             if isinstance(value2, str):
             if isinstance(value2, str):
-                value2 = value2.replace('\n', r'\n')
+                value2 = value2.replace("\n", r"\n")
 
 
             if value2 and not value1:
             if value2 and not value1:
                 diff.append(
                 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:
             elif value1 and not value2:
-                diff.append(_('“%s” unset.') % label)
+                diff.append(_("“%s” unset.") % label)
             else:
             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
     @classmethod
     def create_from_submission(cls, submission, revision_type):
     def create_from_submission(cls, submission, revision_type):
         page = submission.form_page
         page = submission.form_page
         try:
         try:
-            previous = cls.objects.for_submission(
-                submission).latest('created_at')
+            previous = cls.objects.for_submission(submission).latest(
+                "created_at"
+            )
         except cls.DoesNotExist:
         except cls.DoesNotExist:
             previous_data = {}
             previous_data = {}
         else:
         else:
             previous_data = previous.get_data()
             previous_data = previous.get_data()
         filters = cls.get_filters_for(submission)
         filters = cls.get_filters_for(submission)
         data = submission.get_data(raw=True, add_metadata=False)
         data = submission.get_data(raw=True, add_metadata=False)
-        data['status'] = submission.status
+        data["status"] = submission.status
         if revision_type == cls.CREATED:
         if revision_type == cls.CREATED:
-            summary = _('Submission created.')
+            summary = _("Submission created.")
         elif revision_type == cls.DELETED:
         elif revision_type == cls.DELETED:
-            summary = _('Submission deleted.')
+            summary = _("Submission deleted.")
         else:
         else:
             summary = cls.diff_summary(page, previous_data, data)
             summary = cls.diff_summary(page, previous_data, data)
         if not summary:  # Nothing changed.
         if not summary:  # Nothing changed.
             return
             return
         filters.update(
         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)
         return cls.objects.create(**filters)
 
 
     def get_data(self):
     def get_data(self):
         return json.loads(self.data)
         return json.loads(self.data)
 
 
+
 # ORIGINAL NORIPYT CODE.
 # ORIGINAL NORIPYT CODE.
 # We don't want these receivers triggering.
 # We don't want these receivers triggering.
 
 
@@ -632,10 +691,10 @@ class StreamFormMixin:
 
 
     @property
     @property
     def current_step_session_key(self):
     def current_step_session_key(self):
-        return '%s:step' % self.pk
+        return "%s:step" % self.pk
 
 
     def get_steps(self, request=None):
     def get_steps(self, request=None):
-        if not hasattr(self, 'steps'):
+        if not hasattr(self, "steps"):
             steps = Steps(self, request=request)
             steps = Steps(self, request=request)
             if request is None:
             if request is None:
                 return steps
                 return steps
@@ -653,7 +712,7 @@ class StreamFormMixin:
     def get_context(self, request, *args, **kwargs):
     def get_context(self, request, *args, **kwargs):
         context = super().get_context(request, *args, **kwargs)
         context = super().get_context(request, *args, **kwargs)
         self.steps = self.get_steps(request)
         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():
         if step_value is not None and step_value.isdigit():
             self.steps.current = int(step_value) - 1
             self.steps.current = int(step_value) - 1
         form = self.steps.get_current_form()
         form = self.steps.get_current_form()
@@ -662,7 +721,8 @@ class StreamFormMixin:
             step=self.steps.current,
             step=self.steps.current,
             form=form,
             form=form,
             markups_and_bound_fields=list(
             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
         return context
 
 
@@ -671,7 +731,7 @@ class StreamFormMixin:
 
 
     @staticmethod
     @staticmethod
     def get_form_class_bases():
     def get_form_class_bases():
-        return Form,
+        return (Form,)
 
 
     @staticmethod
     @staticmethod
     def get_submission_class():
     def get_submission_class():
@@ -680,25 +740,37 @@ class StreamFormMixin:
     def get_submission(self, request):
     def get_submission(self, request):
         Submission = self.get_submission_class()
         Submission = self.get_submission_class()
         if request.user.is_authenticated:
         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:
             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
             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:
         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
         return user_submission
 
 
     def get_success_url(self):
     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()
         first_child = self.get_children().filter(content_type__in=cts).first()
         if first_child is None:
         if first_child is None:
             return self.url
             return self.url
@@ -707,14 +779,13 @@ class StreamFormMixin:
     def serve_success(self, request, *args, **kwargs):
     def serve_success(self, request, *args, **kwargs):
         url = self.get_success_url()
         url = self.get_success_url()
         if url == self.url:
         if url == self.url:
-            messages.success(request,
-                             _('Successfully submitted the form.'))
+            messages.success(request, _("Successfully submitted the form."))
         return HttpResponseRedirect(url)
         return HttpResponseRedirect(url)
 
 
     def serve(self, request, *args, **kwargs):
     def serve(self, request, *args, **kwargs):
         context = self.get_context(request)
         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()
             is_complete = self.steps.update_data()
             if is_complete:
             if is_complete:
                 return self.serve_success(request, *args, **kwargs)
                 return self.serve_success(request, *args, **kwargs)
@@ -723,22 +794,32 @@ class StreamFormMixin:
 
 
     def get_data_fields(self, by_step=False, add_metadata=True):
     def get_data_fields(self, by_step=False, add_metadata=True):
         if by_step:
         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 = []
         data_fields.extend(self.get_extra_data_fields())
         data_fields.extend(self.get_extra_data_fields())
         if add_metadata:
         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
         return data_fields
 
 
     def get_extra_data_fields(self):
     def get_extra_data_fields(self):
@@ -767,7 +848,7 @@ class ClosingFormMixin(Model):
         if self.closed_template is None:
         if self.closed_template is None:
             template = self.get_template(request, *args, **kwargs)
             template = self.get_template(request, *args, **kwargs)
             base, ext = os.path.splitext(template)
             base, ext = os.path.splitext(template)
-            return '%s_closed%s' % (base, ext)
+            return "%s_closed%s" % (base, ext)
         return self.closed_template
         return self.closed_template
 
 
     def serve_closed(self, request, *args, **kwargs):
     def serve_closed(self, request, *args, **kwargs):
@@ -789,19 +870,24 @@ class FormCompleteMixin:
 
 
     def serve(self, request, *args, **kwargs):
     def serve(self, request, *args, **kwargs):
         form_page = self.get_form_page()
         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)
             return HttpResponseRedirect(form_page.url)
         self.submission = form_page.get_submission(request)
         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 super().serve(request, *args, **kwargs)
         return HttpResponseRedirect(form_page.url)
         return HttpResponseRedirect(form_page.url)
 
 
     def get_context(self, *args, **kwargs):
     def get_context(self, *args, **kwargs):
         context = super().get_context(*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
         return context
 
 
 
 
@@ -812,7 +898,7 @@ class LoginRequiredMixin:
         if self.login_required_template is None:
         if self.login_required_template is None:
             template = self.get_template(request, *args, **kwargs)
             template = self.get_template(request, *args, **kwargs)
             base, ext = os.path.splitext(template)
             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
         return self.login_required_template
 
 
     def serve_login_required(self, request, *args, **kwargs):
     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.urls import path, reverse
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
 from wagtail.contrib.modeladmin.helpers import (
 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.options import ModelAdmin
 from wagtail.contrib.modeladmin.views import IndexView, InstanceSpecificView
 from wagtail.contrib.modeladmin.views import IndexView, InstanceSpecificView
 from wagtail.admin import messages
 from wagtail.admin import messages
@@ -17,7 +21,7 @@ from .models import SessionFormSubmission
 
 
 
 
 class FormIndexView(IndexView):
 class FormIndexView(IndexView):
-    page_title = _('Forms')
+    page_title = _("Forms")
 
 
 
 
 class FormPermissionHelper(PagePermissionHelper):
 class FormPermissionHelper(PagePermissionHelper):
@@ -48,17 +52,21 @@ class FormPermissionHelper(PagePermissionHelper):
 
 
 class FormURLHelper(PageAdminURLHelper):
 class FormURLHelper(PageAdminURLHelper):
     def _get_action_url_pattern(self, action):
     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):
 class FormAdmin(ModelAdmin):
     model = Page
     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
     index_view_class = FormIndexView
     permission_helper_class = FormPermissionHelper
     permission_helper_class = FormPermissionHelper
     url_helper_class = FormURLHelper
     url_helper_class = FormURLHelper
@@ -66,37 +74,48 @@ class FormAdmin(ModelAdmin):
     def get_queryset(self, request):
     def get_queryset(self, request):
         return get_forms_for_user(request.user)
         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>' % (
         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
     all_submissions_link.allow_tags = True
 
 
     def unprocessed_submissions_link(self, obj):
     def unprocessed_submissions_link(self, obj):
         return self.all_submissions_link(
         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
     unprocessed_submissions_link.allow_tags = True
 
 
     def edit_link(self, obj):
     def edit_link(self, obj):
         return '<a href="%s">%s</a>' % (
         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
     edit_link.allow_tags = True
 
 
 
 
 class SubmissionStatusFilter(SimpleListFilter):
 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):
     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:
         for status, verbose_status in SessionFormSubmission.STATUSES:
             if status != SessionFormSubmission.INCOMPLETE:
             if status != SessionFormSubmission.INCOMPLETE:
                 yield status, verbose_status
                 yield status, verbose_status
@@ -105,8 +124,8 @@ class SubmissionStatusFilter(SimpleListFilter):
         status = self.value()
         status = self.value()
         if not status:
         if not status:
             return queryset
             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)
         return queryset.filter(status=status)
 
 
 
 
@@ -129,149 +148,192 @@ class SubmissionPermissionHelper(PermissionHelper):
 
 
 class SubmissionURLHelper(AdminURLHelper):
 class SubmissionURLHelper(AdminURLHelper):
     def _get_action_url_pattern(self, action):
     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):
     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):
 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:
         if classnames_add is None:
             classnames_add = []
             classnames_add = []
         if classnames_exclude is None:
         if classnames_exclude is None:
             classnames_exclude = []
             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 {
         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:
         if classnames_add is None:
             classnames_add = []
             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:
         if classnames_add is None:
             classnames_add = []
             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:
         if classnames_add is None:
             classnames_add = []
             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(
         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)
         pk = getattr(obj, self.opts.pk.attname)
         status_buttons = []
         status_buttons = []
         if obj.status != obj.REVIEWED:
         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:
         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:
         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
         return status_buttons + buttons
 
 
 
 
 class SetStatusView(InstanceSpecificView):
 class SetStatusView(InstanceSpecificView):
     def check_action_permitted(self, user):
     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):
     def get(self, request, *args, **kwargs):
-        status = request.GET.get('status')
+        status = request.GET.get("status")
         if status in dict(self.model.STATUSES):
         if status in dict(self.model.STATUSES):
             previous_status = self.instance.status
             previous_status = self.instance.status
             self.instance.status = status
             self.instance.status = status
             self.instance.save()
             self.instance.save()
             verbose_label = self.instance.get_status_display()
             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:
             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(
                 messages.success(
                     request,
                     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:
         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)
         return redirect(url)
 
 
 
 
 class SubmissionAdmin(ModelAdmin):
 class SubmissionAdmin(ModelAdmin):
     model = SessionFormSubmission
     model = SessionFormSubmission
-    menu_icon = 'form'
+    menu_icon = "form"
     permission_helper_class = SubmissionPermissionHelper
     permission_helper_class = SubmissionPermissionHelper
     url_helper_class = SubmissionURLHelper
     url_helper_class = SubmissionURLHelper
     button_helper_class = SubmissionButtonHelper
     button_helper_class = SubmissionButtonHelper
     set_status_view_class = SetStatusView
     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):
     def register_with_wagtail(self):
-        @hooks.register('register_permissions')
+        @hooks.register("register_permissions")
         def register_permissions():
         def register_permissions():
             return self.get_permissions_for_registration()
             return self.get_permissions_for_registration()
 
 
-        @hooks.register('register_admin_urls')
+        @hooks.register("register_admin_urls")
         def register_admin_urls():
         def register_admin_urls():
             return self.get_admin_urls_for_registration()
             return self.get_admin_urls_for_registration()
 
 
     def get_queryset(self, request):
     def get_queryset(self, request):
         qs = super().get_queryset(request)
         qs = super().get_queryset(request)
         form_pages = get_forms_for_user(request.user)
         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):
     def get_form_page(self, request):
         form_pages = get_forms_for_user(request.user)
         form_pages = get_forms_for_user(request.user)
         try:
         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):
         except (KeyError, TypeError, ValueError, Page.DoesNotExist):
             pass
             pass
 
 
@@ -294,7 +356,7 @@ class SubmissionAdmin(ModelAdmin):
         return fields
         return fields
 
 
     def set_status_view(self, request, instance_pk):
     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
         view_class = self.set_status_view_class
         return view_class.as_view(**kwargs)(request)
         return view_class.as_view(**kwargs)(request)
 
 
@@ -302,9 +364,9 @@ class SubmissionAdmin(ModelAdmin):
         urls = super().get_admin_urls_for_registration()
         urls = super().get_admin_urls_for_registration()
         urls += (
         urls += (
             path(
             path(
-                self.url_helper.get_action_url_pattern('set_status'),
+                self.url_helper.get_action_url_pattern("set_status"),
                 self.set_status_view,
                 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
         return urls

+ 27 - 25
coderedcms/wagtail_hooks.py

@@ -14,29 +14,29 @@ from wagtailcache.cache import clear_cache
 from coderedcms import __version__
 from coderedcms import __version__
 
 
 
 
-@hooks.register('insert_global_admin_css')
+@hooks.register("insert_global_admin_css")
 def global_admin_css():
 def global_admin_css():
     return format_html(
     return format_html(
         '<link rel="stylesheet" type="text/css" href="{}?v={}">',
         '<link rel="stylesheet" type="text/css" href="{}?v={}">',
-        static('coderedcms/css/crx-admin.css'),
+        static("coderedcms/css/crx-admin.css"),
         __version__,
         __version__,
     )
     )
 
 
 
 
-@hooks.register('insert_editor_css')
+@hooks.register("insert_editor_css")
 def editor_css():
 def editor_css():
     return format_html(
     return format_html(
         '<link rel="stylesheet" type="text/css" href="{}?v={}">',
         '<link rel="stylesheet" type="text/css" href="{}?v={}">',
-        static('coderedcms/css/crx-editor.css'),
+        static("coderedcms/css/crx-editor.css"),
         __version__,
         __version__,
     )
     )
 
 
 
 
-@hooks.register('insert_editor_js')
+@hooks.register("insert_editor_js")
 def collapsible_js():
 def collapsible_js():
     return format_html(
     return format_html(
         '<script src="{}?v={}"></script>',
         '<script src="{}?v={}"></script>',
-        static('coderedcms/js/crx-editor.js'),
+        static("coderedcms/js/crx-editor.js"),
         __version__,
         __version__,
     )
     )
 
 
@@ -79,16 +79,16 @@ def clear_wagtailcache(*args, **kwargs):
 # Clear cache whenever pages/snippets are changed. Err on the side of clearing
 # 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
 # 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.
 # 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):
 def codered_forms(user, editable_forms):
     """
     """
     Add our own CoderedFormPage to editable_forms, since wagtail is unaware
     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()
     and wagtail.contrib.forms.get_form_types()
     """
     """
     from coderedcms.models import CoderedFormMixin
     from coderedcms.models import CoderedFormMixin
+
     form_models = [
     form_models = [
-        model for model in get_page_models()
+        model
+        for model in get_page_models()
         if issubclass(model, CoderedFormMixin)
         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 = UserPagePermissionsProxy(user).editable_pages()
     editable_forms = editable_forms.filter(content_type__in=form_types)
     editable_forms = editable_forms.filter(content_type__in=form_types)
@@ -110,7 +110,7 @@ def codered_forms(user, editable_forms):
     return editable_forms
     return editable_forms
 
 
 
 
-@hooks.register('before_serve_document')
+@hooks.register("before_serve_document")
 def serve_document_directly(document, request):
 def serve_document_directly(document, request):
     """
     """
     This hook prevents documents from being downloaded unless
     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)
     content_type, content_encoding = mimetypes.guess_type(document.filename)
     response = HttpResponse(document.file.read(), content_type=content_type)
     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
     return response
 
 
 
 
@@ -128,10 +130,10 @@ class ImportExportMenuItem(MenuItem):
         return request.user.is_superuser
         return request.user.is_superuser
 
 
 
 
-@hooks.register('register_settings_menu_item')
+@hooks.register("register_settings_menu_item")
 def register_import_export_menu_item():
 def register_import_export_menu_item():
     return ImportExportMenuItem(
     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):
 class ColorPickerWidget(forms.TextInput):
-    input_type = 'color'
+    input_type = "color"
 
 
 
 
 class ClassifierSelectWidget(forms.CheckboxSelectMultiple):
 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):
     def optgroups(self, name, value, attrs=None):
         from coderedcms.models.snippet_models import Classifier
         from coderedcms.models.snippet_models import Classifier
+
         classifiers = Classifier.objects.all().select_related()
         classifiers = Classifier.objects.all().select_related()
 
 
         groups = []
         groups = []
@@ -27,15 +28,21 @@ class ClassifierSelectWidget(forms.CheckboxSelectMultiple):
             groups.append((group_name, subgroup, index))
             groups.append((group_name, subgroup, index))
 
 
             for subvalue, sublabel in choices:
             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
                 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:
                 if subindex is not None:
                     subindex += 1
                     subindex += 1
         return groups
         return groups

+ 2 - 2
docs/conf.py

@@ -34,7 +34,7 @@ author = "CodeRed LLC"
 extensions = ["sphinx_wagtail_theme"]
 extensions = ["sphinx_wagtail_theme"]
 
 
 # Add any paths that contain templates here, relative to this directory.
 # Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
+templates_path = ["_templates"]
 
 
 # The suffix(es) of source filenames.
 # The suffix(es) of source filenames.
 # You can specify multiple suffix as a list of string:
 # 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
 # defined by theme itself.  Builtin themes are using these templates by
 # default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
 # default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
 # 'searchbox.html']``.
 # 'searchbox.html']``.
-html_sidebars = {'**': ['searchbox.html', 'globaltoc.html', 'sponsor.html']}
+html_sidebars = {"**": ["searchbox.html", "globaltoc.html", "sponsor.html"]}
 
 
 html_css_files = ["custom.css"]
 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
 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
 .. 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
 .. code-block:: console
 
 
-    $ ./ci/run-flake8.ps1
+    $ flake8 .
 
 
 
 
 Contributor Guidelines
 Contributor Guidelines

+ 10 - 13
pyproject.toml

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

+ 1 - 0
requirements-ci.txt

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

+ 1 - 1
setup.cfg

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

+ 41 - 41
setup.py

@@ -2,61 +2,61 @@ import os
 from setuptools import setup
 from setuptools import setup
 from coderedcms import __version__
 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()
     README = readme.read()
 
 
 # allow setup.py to be run from any path
 # allow setup.py to be run from any path
 os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
 os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
 
 
 setup(
 setup(
-    name='coderedcms',
+    name="coderedcms",
     version=__version__,
     version=__version__,
-    packages=['coderedcms'],
+    packages=["coderedcms"],
     include_package_data=True,
     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=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=[
     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=[
     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={
     entry_points={
-        "console_scripts": [
-            "coderedcms=coderedcms.bin.coderedcms:main"
-        ]
+        "console_scripts": ["coderedcms=coderedcms.bin.coderedcms:main"]
     },
     },
     zip_safe=False,
     zip_safe=False,
 )
 )

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

@@ -26,95 +26,87 @@ BASE_DIR = os.path.dirname(PROJECT_DIR)
 
 
 INSTALLED_APPS = [
 INSTALLED_APPS = [
     # This project
     # This project
-    'website',
-
+    "website",
     # Wagtail CRX (CodeRed Extensions)
     # Wagtail CRX (CodeRed Extensions)
-    'coderedcms',
-    'django_bootstrap5',
-    'modelcluster',
-    'taggit',
-    'wagtailcache',
-    'wagtailseo',
-
+    "coderedcms",
+    "django_bootstrap5",
+    "modelcluster",
+    "taggit",
+    "wagtailcache",
+    "wagtailseo",
     # Wagtail
     # 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
-    '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 = [
 MIDDLEWARE = [
     # Save pages to cache. Must be FIRST.
     # Save pages to cache. Must be FIRST.
-    'wagtailcache.cache.UpdateCacheMiddleware',
-
+    "wagtailcache.cache.UpdateCacheMiddleware",
     # Common functionality
     # 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
     # 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.
     #  Error reporting. Uncomment this to recieve emails when a 404 is triggered.
     # 'django.middleware.common.BrokenLinkEmailsMiddleware',
     # 'django.middleware.common.BrokenLinkEmailsMiddleware',
-
     # CMS functionality
     # CMS functionality
-    'wagtail.contrib.redirects.middleware.RedirectMiddleware',
-
+    "wagtail.contrib.redirects.middleware.RedirectMiddleware",
     # Fetch from cache. Must be LAST.
     # Fetch from cache. Must be LAST.
-    'wagtailcache.cache.FetchFromCacheMiddleware',
+    "wagtailcache.cache.FetchFromCacheMiddleware",
 ]
 ]
 
 
-ROOT_URLCONF = 'mysite.urls'
+ROOT_URLCONF = "mysite.urls"
 
 
 TEMPLATES = [
 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
 # Database
 # https://docs.djangoproject.com/en/3.0/ref/settings/#databases
 # https://docs.djangoproject.com/en/3.0/ref/settings/#databases
 
 
 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 = [
 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/
 # https://docs.djangoproject.com/en/3.0/topics/i18n/
 
 
 # To add or change language of the project, modify the list below.
 # 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
 USE_I18N = True
 
 
@@ -157,38 +147,38 @@ USE_TZ = True
 # https://docs.djangoproject.com/en/3.0/howto/static-files/
 # https://docs.djangoproject.com/en/3.0/howto/static-files/
 
 
 STATICFILES_FINDERS = [
 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
 
 
-LOGIN_URL = 'wagtailadmin_login'
-LOGIN_REDIRECT_URL = 'wagtailadmin_home'
+LOGIN_URL = "wagtailadmin_login"
+LOGIN_REDIRECT_URL = "wagtailadmin_home"
 
 
 
 
 # Wagtail settings
 # Wagtail settings
 
 
-WAGTAIL_SITE_NAME = 'Simple Sweet Desserts Ltd.'
+WAGTAIL_SITE_NAME = "Simple Sweet Desserts Ltd."
 
 
 WAGTAIL_ENABLE_UPDATE_CHECK = False
 WAGTAIL_ENABLE_UPDATE_CHECK = False
 
 
 WAGTAILSEARCH_BACKENDS = {
 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 -
 # 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
 # e.g. in notification emails. Don't include '/admin' or a trailing slash
-WAGTAILADMIN_BASE_URL = 'http://localhost'
+WAGTAILADMIN_BASE_URL = "http://localhost"
 
 
 
 
 # Tags
 # Tags
@@ -196,4 +186,4 @@ WAGTAILADMIN_BASE_URL = 'http://localhost'
 TAGGIT_CASE_INSENSITIVE = True
 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
 DEBUG = True
 
 
 # SECURITY WARNING: keep the secret key used in production secret!
 # 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
 WAGTAIL_CACHE = False
 
 

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

@@ -4,22 +4,22 @@ from .base import *  # noqa
 DEBUG = False
 DEBUG = False
 
 
 # SECURITY WARNING: keep the secret key used in production secret!
 # 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.
 # 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
 # To send email from the server, we recommend django_sendmail_backend
 # Or specify your own email backend such as an SMTP server.
 # Or specify your own email backend such as an SMTP server.
 # https://docs.djangoproject.com/en/3.0/ref/settings/#email-backend
 # 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 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.
 # A list of people who get error notifications.
 ADMINS = [
 ADMINS = [
-    ('Administrator', 'admin@localhost'),
+    ("Administrator", "admin@localhost"),
 ]
 ]
 
 
 # A list in the same format as ADMINS that specifies who should get broken link
 # 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.
 # Requires reloading web server to pick up template changes.
 TEMPLATES = [
 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 = {
 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 = [
 urlpatterns = [
     # Admin
     # Admin
-    path('django-admin/', admin.site.urls),
-    path('admin/', include(coderedadmin_urls)),
-
+    path("django-admin/", admin.site.urls),
+    path("admin/", include(coderedadmin_urls)),
     # Documents
     # Documents
-    path('docs/', include(wagtaildocs_urls)),
-
+    path("docs/", include(wagtaildocs_urls)),
     # Search
     # Search
-    path('search/', include(coderedsearch_urls)),
-
+    path("search/", include(coderedsearch_urls)),
     # For anything not caught by a more specific rule above, hand over to
     # 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 page serving mechanism. This should be the last pattern in
     # the list:
     # 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
     # Alternatively, if you want CMS pages to be served from a subpath
     # of your site, rather than the site root:
     # of your site, rather than the site root:
     #    re_path(r"^pages/", include(codered_urls)),
     #    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):
 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,
     CoderedArticleIndexPage,
     CoderedEmail,
     CoderedEmail,
     CoderedFormPage,
     CoderedFormPage,
-    CoderedWebPage
+    CoderedWebPage,
 )
 )
 from django.db import models
 from django.db import models
 from wagtail.admin.edit_handlers import FieldPanel
 from wagtail.admin.edit_handlers import FieldPanel
 from wagtail.core.fields import RichTextField
 from wagtail.core.fields import RichTextField
 from wagtail.images import get_image_model_string
 from wagtail.images import get_image_model_string
-from wagtail.images.edit_handlers import ImageChooserPanel
 
 
 
 
 class ArticlePage(CoderedArticlePage):
 class ArticlePage(CoderedArticlePage):
     """
     """
     Article, suitable for news or blog content.
     Article, suitable for news or blog content.
     """
     """
+
     class Meta:
     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.
     # 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):
 class ArticleIndexPage(CoderedArticleIndexPage):
     """
     """
     Shows a list of article sub-pages.
     Shows a list of article sub-pages.
     """
     """
+
     class Meta:
     class Meta:
-        verbose_name = 'Article Landing Page'
+        verbose_name = "Article Landing Page"
 
 
     # Override to specify custom index ordering choice/default.
     # Override to specify custom index ordering choice/default.
-    index_query_pagemodel = 'website.ArticlePage'
+    index_query_pagemodel = "website.ArticlePage"
 
 
     # Only allow ArticlePages beneath this page.
     # 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):
 class FormPage(CoderedFormPage):
     """
     """
     A page with an html <form>.
     A page with an html <form>.
     """
     """
+
     class Meta:
     class Meta:
-        verbose_name = 'Form'
+        verbose_name = "Form"
 
 
-    template = 'coderedcms/pages/form_page.html'
+    template = "coderedcms/pages/form_page.html"
 
 
 
 
 class FormPageField(CoderedFormField):
 class FormPageField(CoderedFormField):
     """
     """
     A field that links to a FormPage.
     A field that links to a FormPage.
     """
     """
+
     class Meta:
     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):
 class FormConfirmEmail(CoderedEmail):
     """
     """
     Sends a confirmation email after submitting a FormPage.
     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):
 class WebPage(CoderedWebPage):
@@ -80,26 +84,28 @@ class WebPage(CoderedWebPage):
     General use page with featureful streamfield and SEO attributes.
     General use page with featureful streamfield and SEO attributes.
     Template renders all Navbar and Footer snippets in existence.
     Template renders all Navbar and Footer snippets in existence.
     """
     """
+
     class Meta:
     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):
 class CupcakesIndexPage(CoderedWebPage):
     """
     """
     Landing page for Cupcakes
     Landing page for Cupcakes
     """
     """
+
     class Meta:
     class Meta:
         verbose_name = "Cupcakes Landing Page"
         verbose_name = "Cupcakes Landing Page"
 
 
     # Override to specify custom index ordering choice/default.
     # Override to specify custom index ordering choice/default.
-    index_query_pagemodel = 'website.CupcakesPage'
+    index_query_pagemodel = "website.CupcakesPage"
 
 
     # Only allow CupcakesPages beneath this page.
     # 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):
 class CupcakesPage(CoderedWebPage):
@@ -111,35 +117,30 @@ class CupcakesPage(CoderedWebPage):
         verbose_name = "Cupcakes Page"
         verbose_name = "Cupcakes Page"
 
 
     # Only allow this page to be created beneath an CupcakesIndexPage.
     # 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"
     template = "website/pages/cupcakes_page.html"
 
 
     # The name of the cucpake will be in the page title
     # The name of the cucpake will be in the page title
     description = RichTextField(
     description = RichTextField(
-        verbose_name="Cupcake Description",
-        null=True,
-        blank=True,
-        default=""
+        verbose_name="Cupcake Description", null=True, blank=True, default=""
     )
     )
     photo = models.ForeignKey(
     photo = models.ForeignKey(
         get_image_model_string(),
         get_image_model_string(),
         null=True,
         null=True,
         blank=True,
         blank=True,
         on_delete=models.SET_NULL,
         on_delete=models.SET_NULL,
-        related_name='+',
-        verbose_name='Cupcake Photo',
+        related_name="+",
+        verbose_name="Cupcake Photo",
     )
     )
     DAYS_CHOICES = (
     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(
     days_available = models.CharField(
-        choices=DAYS_CHOICES,
-        max_length=20,
-        default=""
+        choices=DAYS_CHOICES, max_length=20, default=""
     )
     )
 
 
     # Add custom fields to the body
     # Add custom fields to the body

Some files were not shown because too many files changed in this diff