Преглед на файлове

Static analysis code cleanup and CI tooling (#203)

Kevin Cummings преди 5 години
родител
ревизия
560432b497
променени са 41 файла, в които са добавени 665 реда и са изтрити 318 реда
  1. 86 48
      azure-pipelines.yml
  2. 31 0
      ci/run_artifacts.ps1
  3. 20 0
      ci/run_autopep8.ps1
  4. 20 0
      ci/run_black.ps1
  5. 2 1
      coderedcms/admin_urls.py
  6. 5 3
      coderedcms/api/mailchimp.py
  7. 11 4
      coderedcms/bin/coderedcms.py
  8. 67 16
      coderedcms/blocks/__init__.py
  9. 10 9
      coderedcms/blocks/base_blocks.py
  10. 4 2
      coderedcms/blocks/content_blocks.py
  11. 11 4
      coderedcms/blocks/html_blocks.py
  12. 5 3
      coderedcms/blocks/layout_blocks.py
  13. 6 2
      coderedcms/blocks/metadata_blocks.py
  14. 10 8
      coderedcms/blocks/stream_form_blocks.py
  15. 6 4
      coderedcms/blocks/tests/test_blocks.py
  16. 2 1
      coderedcms/fields.py
  17. 17 7
      coderedcms/forms.py
  18. 11 14
      coderedcms/importexport.py
  19. 4 4
      coderedcms/models/__init__.py
  20. 20 16
      coderedcms/models/integration_models.py
  21. 114 73
      coderedcms/models/page_models.py
  22. 36 7
      coderedcms/models/snippet_models.py
  23. 4 3
      coderedcms/models/tests/test_page_models.py
  24. 25 9
      coderedcms/models/wagtailsettings_models.py
  25. 2 2
      coderedcms/project_template/project_name/settings/dev.py
  26. 3 3
      coderedcms/project_template/project_name/settings/prod.py
  27. 1 1
      coderedcms/project_template/project_name/urls.py
  28. 11 7
      coderedcms/settings.py
  29. 1 1
      coderedcms/templatetags/coderedcms_tags.py
  30. 2 6
      coderedcms/tests/settings.py
  31. 0 1
      coderedcms/tests/urls.py
  32. 11 5
      coderedcms/urls.py
  33. 11 1
      coderedcms/utils.py
  34. 19 14
      coderedcms/views.py
  35. 5 8
      coderedcms/wagtail_flexible_forms/models.py
  36. 5 5
      coderedcms/wagtail_flexible_forms/wagtail_hooks.py
  37. 20 8
      coderedcms/wagtail_hooks.py
  38. 2 2
      docs/conf.py
  39. 26 0
      pyproject.toml
  40. 2 2
      setup.cfg
  41. 17 14
      setup.py

+ 86 - 48
azure-pipelines.yml

@@ -3,7 +3,7 @@
 # Add steps that analyze code, save build artifacts, deploy, and more:
 # https://docs.microsoft.com/azure/devops/pipelines/languages/python
 
-# NTOES:
+# NOTES:
 #
 # Display name of each step should be prefixed with one of the following:
 #   CR-QC: for quality control measures.
@@ -14,53 +14,91 @@
 # Use PowerShell Core for any scripts so they are re-usable across windows/mac/linux.
 #
 
+
 trigger:
   - master
 
-pool:
-  vmImage: 'ubuntu-latest'
-strategy:
-  matrix:
-    py3.5:
-      PYTHON_VERSION: '3.5'
-    py3.6:
-      PYTHON_VERSION: '3.6'
-    py3.7:
-      PYTHON_VERSION: '3.7'
-
-steps:
-- task: UsePythonVersion@0
-  displayName: 'Use Python version'
-  inputs:
-    versionSpec: '$(PYTHON_VERSION)'
-    architecture: 'x64'
-
-- script: |
-    python -m pip install -e ./[ci]
-  displayName: 'CR-QC: Install coderedcms from local repo'
-
-- script: |
-    coderedcms start testproject --name="Test Project" --domain="www.example.com"
-  displayName: 'CR-QC: Create starter project from template'
-
-- script: |
-    pytest coderedcms/ --ds=coderedcms.tests.settings --junitxml=junit/test-results.xml --cov=coderedcms --cov-report=xml --cov-report=html
-  displayName: 'CR-QC: Run unit tests'
-
-- pwsh: |
-    & ci/run_flake8.ps1
-  displayName: 'CR-QC: Static analysis'
-
-- task: PublishTestResults@2
-  displayName: 'Publish unit test report'
-  condition: succeededOrFailed()
-  inputs:
-    testResultsFiles: '**/test-*.xml'
-    testRunTitle: 'Publish test results for Python $(python.version)'
-
-- task: PublishCodeCoverageResults@1
-  displayName: 'Publish code coverage report'
-  condition: succeededOrFailed()
-  inputs:
-    codeCoverageTool: Cobertura
-    summaryFileLocation: '$(System.DefaultWorkingDirectory)/coverage.xml'
+jobs:
+- job: tests
+  displayName: Unit Tests
+  pool:
+    vmImage: 'ubuntu-latest'
+  strategy:
+    matrix:
+      py3.5:
+        PYTHON_VERSION: '3.5'
+      py3.6:
+        PYTHON_VERSION: '3.6'
+      py3.7:
+        PYTHON_VERSION: '3.7'
+  
+  steps:
+  - task: UsePythonVersion@0
+    displayName: 'Use Python version'
+    inputs:
+      versionSpec: '$(PYTHON_VERSION)'
+      architecture: 'x64'
+
+  - script: |
+      python -m pip install -e ./[ci_tests]
+    displayName: 'CR-QC: Install coderedcms from local repo'
+
+  - script: |
+      coderedcms start testproject --name="Test Project" --domain="www.example.com"
+    displayName: 'CR-QC: Create starter project from template'
+
+  - script: |
+      pytest coderedcms/ --ds=coderedcms.tests.settings --junitxml=junit/test-results.xml --cov=coderedcms --cov-report=xml --cov-report=html
+    displayName: 'CR-QC: Run unit tests'
+
+  - task: PublishTestResults@2
+    displayName: 'Publish unit test report'
+    condition: succeededOrFailed()
+    inputs:
+      testResultsFiles: '**/test-*.xml'
+      testRunTitle: 'Publish test results for Python $(python.version)'
+
+  - task: PublishCodeCoverageResults@1
+    displayName: 'Publish code coverage report'
+    condition: succeededOrFailed()
+    inputs:
+      codeCoverageTool: Cobertura
+      summaryFileLocation: '$(System.DefaultWorkingDirectory)/coverage.xml'
+
+  - task: DownloadPipelineArtifact@2
+    displayName: 'Download code coverage from latest build'
+    inputs:
+      source: 'specific'
+      path: '$(Agent.WorkFolder)/artifacts'
+      project: 'cc52b8d8-3ae5-466e-b771-56f3b4749f1f'
+      pipeline: 1
+      runVersion: 'latestFromBranch'
+      runBranch: 'refs/heads/master'
+
+  - pwsh: |
+      & ci/run_artifacts.ps1
+    displayName: 'Compare code coverage to master'
+
+- job: style
+  displayName: Static Analysis
+  pool:
+    vmImage: 'ubuntu-latest'
+
+  steps:
+  - task: UsePythonVersion@0
+    displayName: 'Use Python version'
+    inputs:
+      versionSpec: '3.7'
+      architecture: 'x64'
+
+  - script: |
+      python -m pip install -e ./[ci_style]
+    displayName: 'CR-QC: Install coderedcms from local repo'
+
+  - script: |
+      coderedcms start testproject
+    displayName: 'CR-QC: Generate a test project'
+
+  - pwsh: |
+      & ci/run_flake8.ps1
+    displayName: 'CR-QC: Static analysis (flake8)'

+ 31 - 0
ci/run_artifacts.ps1

@@ -0,0 +1,31 @@
+if (Test-Path -Path "/home/vsts/work/artifacts/Code Coverage Report_*/summary*/coverage.xml") {
+    [xml]$MasterXML = Get-Content "/home/vsts/work/artifacts/Code Coverage Report_*/summary*/coverage.xml"
+} else {
+    Write-Host "No code coverage from previous build. Exiting pipeline." -ForegroundColor Red
+    exit 1
+}
+
+[xml]$BranchXML = Get-Content .\coverage.xml
+
+$masterlinerate = [math]::Abs([math]::Round([decimal]$MasterXML.coverage.'line-rate' * 100, 2))
+$branchlinerate = [math]::Abs([math]::Round([decimal]$BranchXML.coverage.'line-rate' * 100, 2))
+
+Write-Output "Old line coverage rate: $masterlinerate%"
+Write-Output "New line coverage rate: $branchlinerate%"
+
+if ($masterlinerate -eq 0) {
+    $change = "Infinite"
+} else {
+    $change = [math]::Abs([math]::Round((($branchlinerate - $masterlinerate) / $masterlinerate) * 100, 2))
+}
+
+if ($branchlinerate -gt $masterlinerate) {
+    Write-Host "Code coverage has increased by $change%. Build passed." -ForegroundColor Green
+    exit 0
+} elseif ($branchlinerate -eq $masterlinerate) {
+    Write-Host "Code coverage has not changed. Build passed." -ForegroundColor Green
+    exit 0
+} else {
+    Write-Host "Code coverage as decreased by $change%. Code coverage must be greater than or equal to the previous build to pass." -ForegroundColor Red
+    exit 2
+}

+ 20 - 0
ci/run_autopep8.ps1

@@ -0,0 +1,20 @@
+$ExitCode = 0
+$GitDiff = git diff --name-only origin/master
+$GitDiffPep = Write-Output $GitDiff | Select-String -Pattern ".*\.py" | Select-String -NotMatch ".*/project_template/.*"
+# If there is no diff between master, then run everything.
+if ( $GitDiffPep -eq $null ) {
+    autopep8 -r --diff coderedcms/
+    if ($LastExitCode -ne 0) { $ExitCode = $LastExitCode }
+}
+# Else run just the diff.
+else {
+    autopep8 -r --diff $GitDiffPep
+    if ($LastExitCode -ne 0) { $ExitCode = $LastExitCode }
+    # If the project_template changed, then run the testproject too.
+    $GitDiffTempl = Write-Output $GitDiff | Select-String -Pattern ".*/project_template/.*"
+    if ( $GitDiffTempl -ne $null ) {
+        #autopep8 -r --diff testproject/
+        if ($LastExitCode -ne 0) { $ExitCode = $LastExitCode }
+    }
+}
+exit $ExitCode

+ 20 - 0
ci/run_black.ps1

@@ -0,0 +1,20 @@
+$ExitCode = 0
+$GitDiff = git diff --name-only origin/master
+$GitDiffBlack = Write-Output $GitDiff | Select-String -Pattern ".*\.py" | Select-String -NotMatch ".*/project_template/.*"
+# If there is no diff between master, then black everything.
+if ( $GitDiffBlack -eq $null ) {
+    black --check .
+    if ($LastExitCode -ne 0) { $ExitCode = $LastExitCode }
+}
+# Else black just the diff.
+else {
+    black --check $GitDiffBlack
+    if ($LastExitCode -ne 0) { $ExitCode = $LastExitCode }
+    # If the project_template changed, then black the testproject too.
+    $GitDiffTempl = Write-Output $GitDiff | Select-String -Pattern ".*/project_template/.*"
+    if ( $GitDiffTempl -ne $null ) {
+        black --check testproject/
+        if ($LastExitCode -ne 0) { $ExitCode = $LastExitCode }
+    }
+}
+exit $ExitCode

+ 2 - 1
coderedcms/admin_urls.py

@@ -5,7 +5,8 @@ from coderedcms.views import import_pages_from_csv_file
 
 
 urlpatterns = [
-    path('codered/import-export/import_from_csv/', import_pages_from_csv_file, name="import_from_csv"),
+    path('codered/import-export/import_from_csv/',
+         import_pages_from_csv_file, name="import_from_csv"),
     re_path(r'', include(wagtailadmin_urls)),
     re_path(r'', include(wagtailimportexport_urls)),
 ]

+ 5 - 3
coderedcms/api/mailchimp.py

@@ -3,6 +3,7 @@ from coderedcms.models.wagtailsettings_models import MailchimpApiSettings
 
 import requests
 
+
 class MailchimpApi:
     user_string = "Website"
     proto_base_url = "https://{0}.api.mailchimp.com/3.0/"
@@ -40,17 +41,18 @@ class MailchimpApi:
         return json_response
 
     def get_merge_fields_for_list(self, list_id):
-        endpoint = "lists/{0}/merge-fields?fields=merge_fields.tag,merge_fields.merge_id,merge_fields.name".format(list_id)
+        endpoint = "lists/{0}/merge-fields?fields=merge_fields.tag,merge_fields.merge_id,merge_fields.name".format(list_id)  # noqa
         json_response = self._get(endpoint)
         return json_response
 
     def get_interest_categories_for_list(self, list_id):
-        endpoint = "lists/{0}/interest-categories?fields=categories.id,categories.title".format(list_id)
+        endpoint = "lists/{0}/interest-categories?fields=categories.id,categories.title".format(
+            list_id)
         json_response = self._get(endpoint)
         return json_response
 
     def get_interests_for_interest_category(self, list_id, interest_category_id):
-        endpoint = "lists/{0}/interest-categories/{1}/interests?fields=interests.id,interests.name".format(list_id, interest_category_id)
+        endpoint = "lists/{0}/interest-categories/{1}/interests?fields=interests.id,interests.name".format(list_id, interest_category_id)  # noqa
         json_response = self._get(endpoint)
         return json_response
 

+ 11 - 4
coderedcms/bin/coderedcms.py

@@ -2,7 +2,6 @@
 import os
 import sys
 
-from django.core.management import ManagementUtility
 from django.core.management.templates import TemplateCommand
 from django.core.management.utils import get_random_secret_key
 
@@ -11,7 +10,9 @@ CURRENT_PYTHON = sys.version_info[:2]
 REQUIRED_PYTHON = (3, 4)
 
 if CURRENT_PYTHON < REQUIRED_PYTHON:
-    sys.stderr.write("This version of Wagtail requires Python {}.{} or above - you are running {}.{}\n".format(*(REQUIRED_PYTHON + CURRENT_PYTHON)))
+    sys.stderr.write(
+        "This version of Wagtail requires Python {}.{} or above - you are running {}.{}\n".format(*(REQUIRED_PYTHON + CURRENT_PYTHON))  # noqa
+    )
     sys.exit(1)
 
 
@@ -23,8 +24,14 @@ class CreateProject(TemplateCommand):
     missing_args_message = "You must provide a project name."
 
     def add_arguments(self, parser):
-        parser.add_argument('--sitename', help='Human readable name of your website or brand, e.g. "Mega Corp Inc."')
-        parser.add_argument('--domain', help='Domain that will be used for your website in production, e.g. "www.example.com"')
+        parser.add_argument(
+            '--sitename',
+            help='Human readable name of your website or brand, e.g. "Mega Corp Inc."'
+        )
+        parser.add_argument(
+            '--domain',
+            help='Domain that will be used for your website in production, e.g. "www.example.com"'
+        )
         super().add_arguments(parser)
 
     def handle(self, **options):

+ 67 - 16
coderedcms/blocks/__init__.py

@@ -5,18 +5,69 @@ single `blocks` module.
 """
 
 from django.utils.translation import ugettext_lazy as _
-from wagtail.core.blocks import CharBlock, StreamBlock, StructBlock
-
-from coderedcms.wagtail_flexible_forms.blocks import FormStepBlock, FormStepsBlock
-
-from .stream_form_blocks import * #noqa
-from .base_blocks import * #noqa
-from .html_blocks import * #noqa
-from .metadata_blocks import * #noqa
-from .content_blocks import * #noqa
-from .layout_blocks import * #noqa
 
+from wagtail.core import blocks
 
+from .stream_form_blocks import (
+    CoderedStreamFormCharFieldBlock,
+    CoderedStreamFormCheckboxesFieldBlock,
+    CoderedStreamFormCheckboxFieldBlock,
+    CoderedStreamFormDateFieldBlock,
+    CoderedStreamFormDateTimeFieldBlock,
+    CoderedStreamFormDropdownFieldBlock,
+    CoderedStreamFormFileFieldBlock,
+    CoderedStreamFormImageFieldBlock,
+    CoderedStreamFormNumberFieldBlock,
+    CoderedStreamFormRadioButtonsFieldBlock,
+    CoderedStreamFormStepBlock,
+    CoderedStreamFormTextFieldBlock,
+    CoderedStreamFormTimeFieldBlock
+)
+from .html_blocks import (
+    ButtonBlock,
+    EmbedGoogleMapBlock,
+    ImageBlock,
+    ImageLinkBlock,
+    DownloadBlock,
+    EmbedVideoBlock,
+    PageListBlock,
+    PagePreviewBlock,
+    QuoteBlock,
+    RichTextBlock,
+    TableBlock
+)
+from .content_blocks import (  # noqa
+    CardBlock,
+    CarouselBlock,
+    ContentWallBlock,
+    ImageGalleryBlock,
+    ModalBlock,
+    NavDocumentLinkWithSubLinkBlock,
+    NavExternalLinkWithSubLinkBlock,
+    NavPageLinkWithSubLinkBlock,
+    PriceListBlock,
+    ReusableContentBlock
+)
+from .layout_blocks import (
+    CardGridBlock,
+    GridBlock,
+    HeroBlock
+)
+from .metadata_blocks import (  # noqa
+    OpenHoursBlock,
+    StructuredDataActionBlock
+)
+from .base_blocks import (  # noqa
+    BaseBlock,
+    BaseLayoutBlock,
+    BaseLinkBlock,
+    ClassifierTermChooserBlock,
+    CoderedAdvColumnSettings,
+    CoderedAdvSettings,
+    CoderedAdvTrackingSettings,
+    CollectionChooserBlock,
+    MultiSelectBlock
+)
 
 # Collections of blocks commonly used together.
 
@@ -59,14 +110,14 @@ LAYOUT_STREAMBLOCKS = [
     ('hero', HeroBlock([
         ('row', GridBlock(CONTENT_STREAMBLOCKS)),
         ('cardgrid', CardGridBlock([
-            ('card', CardBlock()),])
-        ),
-        ('html', blocks.RawHTMLBlock(icon='code', classname='monospace', label=_('HTML'))),])
-    ),
+            ('card', CardBlock()),
+        ])),
+        ('html', blocks.RawHTMLBlock(icon='code', classname='monospace', label=_('HTML'))),
+    ])),
     ('row', GridBlock(CONTENT_STREAMBLOCKS)),
     ('cardgrid', CardGridBlock([
-        ('card', CardBlock()),])
-    ),
+        ('card', CardBlock()),
+    ])),
     ('html', blocks.RawHTMLBlock(icon='code', classname='monospace', label=_('HTML'))),
 ]
 

+ 10 - 9
coderedcms/blocks/base_blocks.py

@@ -21,6 +21,7 @@ class MultiSelectBlock(blocks.FieldBlock):
     Renders as MultipleChoiceField, used for adding checkboxes,
     radios, or multiselect inputs in the streamfield.
     """
+
     def __init__(self, required=True, help_text=None, choices=None, widget=None, **kwargs):
         self.field = forms.MultipleChoiceField(
             required=required,
@@ -42,9 +43,9 @@ class ClassifierTermChooserBlock(blocks.FieldBlock):
     widget = forms.Select
 
     def __init__(self, required=False, label=None, help_text=None, *args, **kwargs):
-        self._required=required
-        self._help_text=help_text
-        self._label=label
+        self._required = required
+        self._help_text = help_text
+        self._label = label
         super().__init__(*args, **kwargs)
 
     @cached_property
@@ -86,9 +87,9 @@ class CollectionChooserBlock(blocks.FieldBlock):
     widget = forms.Select
 
     def __init__(self, required=False, label=None, help_text=None, *args, **kwargs):
-        self._required=required
-        self._help_text=help_text
-        self._label=label
+        self._required = required
+        self._help_text = help_text
+        self._label = label
         super().__init__(*args, **kwargs)
 
     @cached_property
@@ -207,7 +208,7 @@ class CoderedAdvColumnSettings(CoderedAdvSettings):
         default=cr_settings['FRONTEND_COL_BREAK_DEFAULT'],
         required=False,
         verbose_name=_('Column Breakpoint'),
-        help_text=_('Screen size at which the column will expand horizontally or stack vertically.'),
+        help_text=_('Screen size at which the column will expand horizontally or stack vertically.'),  # noqa
     )
 
 
@@ -227,7 +228,7 @@ class BaseBlock(blocks.StructBlock):
         """
         klassname = self.__class__.__name__.lower()
         choices = cr_settings['FRONTEND_TEMPLATES_BLOCKS'].get('*', ()) + \
-                  cr_settings['FRONTEND_TEMPLATES_BLOCKS'].get(klassname, ())
+            cr_settings['FRONTEND_TEMPLATES_BLOCKS'].get(klassname, ())
 
         if not local_blocks:
             local_blocks = ()
@@ -285,6 +286,7 @@ class LinkStructValue(blocks.StructValue):
         else:
             return ext
 
+
 class BaseLinkBlock(BaseBlock):
     """
     Common attributes for creating a link within the CMS.
@@ -307,4 +309,3 @@ class BaseLinkBlock(BaseBlock):
 
     class Meta:
         value_class = LinkStructValue
-

+ 4 - 2
coderedcms/blocks/content_blocks.py

@@ -91,7 +91,7 @@ class ModalBlock(ButtonMixin, BaseLayoutBlock):
     )
     footer = blocks.StreamBlock(
         [
-            ('text', blocks.CharBlock(icon='fa-file-text-o', max_length=255, label=_('Simple Text'))),
+            ('text', blocks.CharBlock(icon='fa-file-text-o', max_length=255, label=_('Simple Text'))),  # noqa
             ('button', ButtonBlock()),
         ],
         required=False,
@@ -263,8 +263,10 @@ class ContentWallBlock(BaseBlock):
         required=False,
         default=False,
         verbose_name=_('Show content walls on children pages?'),
-        help_text=_('If this is checked, the content walls will be displayed on all children pages of this page.')
+        help_text=_(
+            'If this is checked, the content walls will be displayed on all children pages of this page.')  # noqa
     )
+
     class Meta:
         icon = 'fa-stop'
         label = _('Content Wall')

+ 11 - 4
coderedcms/blocks/html_blocks.py

@@ -79,7 +79,9 @@ class EmbedGoogleMapBlock(BaseBlock):
         required=False,
         default=14,
         label=_('Map zoom level'),
-        help_text=_('Requires API key to use zoom. 1: World, 5: Landmass/continent, 10: City, 15: Streets, 20: Buildings')
+        help_text=_(
+            "Requires API key to use zoom. 1: World, 5: Landmass/continent, 10: City, 15: Streets, 20: Buildings"  # noqa
+        )
     )
 
     class Meta:
@@ -199,7 +201,9 @@ class PageListBlock(BaseBlock):
     indexed_by = blocks.PageChooserBlock(
         required=True,
         label=_('Parent page'),
-        help_text=_('Show a preview of pages that are children of the selected page. Uses ordering specified in the page’s LAYOUT tab.'),
+        help_text=_(
+            "Show a preview of pages that are children of the selected page. Uses ordering specified in the page’s LAYOUT tab."  # noqa
+        ),
     )
     classified_by = ClassifierTermChooserBlock(
         required=False,
@@ -233,9 +237,12 @@ class PageListBlock(BaseBlock):
             if value['classified_by']:
                 try:
                     pages = pages.filter(classifier_terms=value['classified_by'])
-                except:
+                except AttributeError:
                     # `pages` is not a queryset, or is not a queryset of CoderedPage.
-                    logger.warning("Tried to filter by ClassifierTerm in PageListBlock, but <%s.%s ('%s')>.get_index_children() did not return a queryset or is not a queryset of CoderedPage models.", indexer._meta.app_label, indexer.__class__.__name__, indexer.title)
+                    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
+                    )
         else:
             pages = indexer.get_children().live()
 

+ 5 - 3
coderedcms/blocks/layout_blocks.py

@@ -12,7 +12,7 @@ from coderedcms.settings import cr_settings
 from .base_blocks import BaseLayoutBlock, CoderedAdvColumnSettings
 
 
-### Level 1 layout blocks
+# Level 1 layout blocks
 
 
 class ColumnBlock(BaseLayoutBlock):
@@ -50,11 +50,12 @@ class GridBlock(BaseLayoutBlock):
 
     def __init__(self, local_blocks=None, **kwargs):
         super().__init__(
-            local_blocks = [
+            local_blocks=[
                 ('content', ColumnBlock(local_blocks))
             ]
         )
 
+
 class CardGridBlock(BaseLayoutBlock):
     """
     Renders a row of cards.
@@ -83,7 +84,8 @@ class HeroBlock(BaseLayoutBlock):
     is_parallax = blocks.BooleanBlock(
         required=False,
         label=_('Parallax Effect'),
-        help_text=_('Background images scroll slower than foreground images, creating an illusion of depth.'),
+        help_text=_(
+            'Background images scroll slower than foreground images, creating an illusion of depth.'),  # noqa
     )
     background_image = ImageChooserBlock(required=False)
     tile_image = blocks.BooleanBlock(

+ 6 - 2
coderedcms/blocks/metadata_blocks.py

@@ -23,6 +23,7 @@ class OpenHoursValue(blocks.StructValue):
         """
         return json.dumps(self['days'])
 
+
 class OpenHoursBlock(blocks.StructBlock):
     """
     Holds day and time combination for business open hours.
@@ -62,7 +63,8 @@ class StructuredDataActionBlock(blocks.StructBlock):
     target = blocks.URLBlock(verbose_name=_('Target URL'))
     language = blocks.CharBlock(
         verbose_name=_('Language'),
-        help_text=_('If the action is offered in multiple languages, create separate actions for each language.'),
+        help_text=_(
+            'If the action is offered in multiple languages, create separate actions for each language.'),  # noqa
         default='en-US'
     )
     result_type = blocks.ChoiceBlock(
@@ -80,7 +82,9 @@ class StructuredDataActionBlock(blocks.StructBlock):
         required=False,
         verbose_name=_('Additional action markup'),
         classname='monospace',
-        help_text=_('Additional JSON-LD inserted into the Action dictionary. Must be properties of https://schema.org/Action.')
+        help_text=_(
+            "Additional JSON-LD inserted into the Action dictionary. Must be properties of https://schema.org/Action."  # noqa
+        )
     )
 
     class Meta:

+ 10 - 8
coderedcms/blocks/stream_form_blocks.py

@@ -4,9 +4,9 @@ from wagtail.core import blocks
 from coderedcms.wagtail_flexible_forms import blocks as form_blocks
 from coderedcms.blocks.base_blocks import BaseBlock, CoderedAdvSettings
 from coderedcms.forms import (
-    CoderedDateField, CoderedDateInput, 
-    CoderedDateTimeField, CoderedDateTimeInput, 
-    CoderedTimeField, CoderedTimeInput, 
+    CoderedDateField, CoderedDateInput,
+    CoderedDateTimeField, CoderedDateTimeInput,
+    CoderedTimeField, CoderedTimeInput,
     SecureFileField
 )
 
@@ -17,19 +17,21 @@ class CoderedFormAdvSettings(CoderedAdvSettings):
         required=False,
         max_length=255,
         label=_('Condition Trigger ID'),
-        help_text=_('The "Custom ID" of another field that that will trigger this field to be shown/hidden.')
+        help_text=_(
+            'The "Custom ID" of another field that that will trigger this field to be shown/hidden.')  # noqa
     )
     condition_trigger_value = blocks.CharBlock(
         required=False,
         max_length=255,
         label=_('Condition Trigger Value'),
-        help_text=_('The value of the field in "Condition Trigger ID" that will trigger this field to be shown.')
+        help_text=_(
+            'The value of the field in "Condition Trigger ID" that will trigger this field to be shown.')  # noqa
     )
 
 
 class FormBlockMixin(BaseBlock):
     class Meta:
-        abstract=True
+        abstract = True
 
     advsettings_class = CoderedFormAdvSettings
 
@@ -84,7 +86,7 @@ class CoderedStreamFormDateFieldBlock(form_blocks.DateFieldBlock, FormBlockMixin
     class Meta:
         label = _("Date")
         icon = "fa-calendar"
-    
+
     field_class = CoderedDateField
     widget = CoderedDateInput
 
@@ -126,7 +128,7 @@ class CoderedStreamFormStepBlock(form_blocks.FormStepBlock):
 
     def __init__(self, local_blocks=None, **kwargs):
         super().__init__(
-            local_blocks = [
+            local_blocks=[
                 ('form_fields', blocks.StreamBlock(local_blocks))
             ]
         )

+ 6 - 4
coderedcms/blocks/tests/test_blocks.py

@@ -1,19 +1,21 @@
 from coderedcms.blocks import base_blocks
-from django.test import SimpleTestCase, TestCase
+from django.test import SimpleTestCase
 
 from wagtail.tests.utils import WagtailTestUtils
 
+
 class TestMultiSelectBlock(WagtailTestUtils, SimpleTestCase):
     def test_render_single_choice(self):
-        block = base_blocks.MultiSelectBlock(choices=[('tea', 'Tea'), ('coffee', 'Coffee'), ('water', 'Water')])
+        block = base_blocks.MultiSelectBlock(
+            choices=[('tea', 'Tea'), ('coffee', 'Coffee'), ('water', 'Water')])
         html = block.render_form(['tea'])
         self.assertInHTML('<option value="tea" selected>Tea</option>', html)
         self.assertTrue(html.count('selected'), 1)
 
     def test_render_multi_choice(self):
-        block = base_blocks.MultiSelectBlock(choices=[('tea', 'Tea'), ('coffee', 'Coffee'), ('water', 'Water')])
+        block = base_blocks.MultiSelectBlock(
+            choices=[('tea', 'Tea'), ('coffee', 'Coffee'), ('water', 'Water')])
         html = block.render_form(['coffee', 'tea'])
         self.assertInHTML('<option value="tea" selected>Tea</option>', html)
         self.assertInHTML('<option value="coffee" selected>Coffee</option>', html)
         self.assertTrue(html.count('selected'), 2)
-

+ 2 - 1
coderedcms/fields.py

@@ -2,7 +2,8 @@ from django.db import models
 
 from coderedcms.widgets import ColorPickerWidget
 
-class ColorField(models.CharField):    
+
+class ColorField(models.CharField):
     def __init__(self, *args, **kwargs):
         kwargs['max_length'] = 255
         super(ColorField, self).__init__(*args, **kwargs)

+ 17 - 7
coderedcms/forms.py

@@ -65,12 +65,12 @@ class SecureFileField(forms.FileField):
 
     def _check_whitelist(self, value):
         if cr_settings['PROTECTED_MEDIA_UPLOAD_WHITELIST']:
-            if os.path.splitext(value.name)[1].lower() not in cr_settings['PROTECTED_MEDIA_UPLOAD_WHITELIST']:
+            if os.path.splitext(value.name)[1].lower() not in cr_settings['PROTECTED_MEDIA_UPLOAD_WHITELIST']:  # noqa
                 raise ValidationError(self.error_messages['whitelist_file'])
 
     def _check_blacklist(self, value):
         if cr_settings['PROTECTED_MEDIA_UPLOAD_BLACKLIST']:
-            if os.path.splitext(value.name)[1].lower() in cr_settings['PROTECTED_MEDIA_UPLOAD_BLACKLIST']:
+            if os.path.splitext(value.name)[1].lower() in cr_settings['PROTECTED_MEDIA_UPLOAD_BLACKLIST']:  # noqa
                 raise ValidationError(self.error_messages['blacklist_file'])
 
 
@@ -79,6 +79,7 @@ class SecureFileField(forms.FileField):
 class CoderedDateInput(forms.DateInput):
     template_name = 'coderedcms/formfields/date.html'
 
+
 class CoderedDateField(forms.DateField):
     widget = CoderedDateInput()
 
@@ -88,6 +89,7 @@ class CoderedDateField(forms.DateField):
 class CoderedDateTimeInput(forms.DateTimeInput):
     template_name = 'coderedcms/formfields/datetime.html'
 
+
 class CoderedDateTimeField(forms.DateTimeField):
     widget = CoderedDateTimeInput()
     input_formats = ['%Y-%m-%dT%H:%M', '%m/%d/%Y %I:%M %p', '%m/%d/%Y %I:%M%p', '%m/%d/%Y %H:%M']
@@ -98,6 +100,7 @@ class CoderedDateTimeField(forms.DateTimeField):
 class CoderedTimeInput(forms.TimeInput):
     template_name = 'coderedcms/formfields/time.html'
 
+
 class CoderedTimeField(forms.TimeField):
     widget = CoderedTimeInput()
     input_formats = ['%H:%M', '%I:%M %p', '%I:%M%p']
@@ -122,8 +125,6 @@ class CoderedFormBuilder(FormBuilder):
 
 
 class CoderedSubmissionsListView(WagtailSubmissionsListView):
-
-    
     def get_csv_response(self, context):
         filename = self.get_csv_filename()
         response = HttpResponse(content_type='text/csv; charset=utf-8')
@@ -145,7 +146,7 @@ class CoderedFormField(AbstractFormField):
     class Meta:
         abstract = True
 
-    field_type = models.CharField(verbose_name=_('field type'), max_length=16, choices=FORM_FIELD_CHOICES, blank=True)
+    field_type = models.CharField(verbose_name=_('field type'), max_length=16, choices=FORM_FIELD_CHOICES, blank=True)  # noqa
 
 
 class SearchForm(forms.Form):
@@ -161,11 +162,20 @@ class SearchForm(forms.Form):
         label=_('Page type'),
     )
 
+
 def get_page_model_choices():
     """
-    Returns a list of tuples of all creatable Codered pages in the format of ("Custom Codered Page", "CustomCoderedPage")
+    Returns a list of tuples of all creatable Codered pages
+    in the format of ("Custom Codered Page", "CustomCoderedPage")
     """
     from coderedcms.models import get_page_models
     return (
-        (page.__name__, re.sub(r'((?<=[a-z])[A-Z]|(?<!\A)[A-Z](?=[a-z]))', r' \1', page.__name__)) for page in get_page_models() if page.is_creatable
+        (
+            page.__name__,
+            re.sub(
+                r'((?<=[a-z])[A-Z]|(?<!\A)[A-Z](?=[a-z]))',
+                r' \1',
+                page.__name__
+            )
+        ) for page in get_page_models() if page.is_creatable
     )

+ 11 - 14
coderedcms/importexport.py

@@ -36,11 +36,12 @@ def import_pages(import_data, parent_page):
     page_content_type = ContentType.objects.get_for_model(Page)
 
     for page_record in import_data['pages']:
-        # build a base Page instance from the exported content (so that we pick up its title and other
-        # core attributes)
+        # build a base Page instance from the exported content
+        # (so that we pick up its title and other core attributes)
         page = Page.from_serializable_data(page_record['content'])
 
-        # clear id and treebeard-related fields so that they get reassigned when we save via add_child
+        # clear id and treebeard-related fields so that
+        # they get reassigned when we save via add_child
         page.id = None
         page.path = None
         page.depth = None
@@ -62,7 +63,11 @@ def import_pages(import_data, parent_page):
         # Raises LookupError exception if there is no matching model
         model = apps.get_model(page_record['app_label'], page_record['model'])
 
-        specific_page = model.from_serializable_data(page_record['content'], check_fks=False, strict_fks=False)
+        specific_page = model.from_serializable_data(
+            page_record['content'],
+            check_fks=False,
+            strict_fks=False
+        )
         base_page = pages_by_original_id[specific_page.id]
         specific_page.page_ptr = base_page
         specific_page.__dict__.update(base_page.__dict__)
@@ -74,16 +79,8 @@ def import_pages(import_data, parent_page):
 
 
 def convert_csv_to_json(csv_file, page_type):
-    pages_json = {
-        "pages": []
-    }
-    default_page_data = {
-        "app_label": "website",
-        "content": {
-            "pk": None
-        },
-        "model": page_type
-    }
+    pages_json = {"pages": []}
+    default_page_data = {"app_label": "website", "content": {"pk": None}, "model": page_type}
 
     pages_csv_dict = csv.DictReader(csv_file)
     for row in pages_csv_dict:

+ 4 - 4
coderedcms/models/__init__.py

@@ -4,7 +4,7 @@ into files based on their purpose, but provide them all via
 a single `models` module.
 """
 
-from .integration_models import * #noqa
-from .page_models import * #noqa
-from .snippet_models import * #noqa
-from .wagtailsettings_models import * #noqa
+from .integration_models import *  # noqa
+from .page_models import *  # noqa
+from .snippet_models import *  # noqa
+from .wagtailsettings_models import *  # noqa

+ 20 - 16
coderedcms/models/integration_models.py

@@ -1,13 +1,11 @@
 from django.db import models
-from django.forms.widgets import Select, Input
+from django.forms.widgets import Input
 from django.template import Context, Template
 from django.template.loader import render_to_string
 from django.utils.translation import ugettext_lazy as _
 
 from wagtail.admin.edit_handlers import FieldPanel
 from wagtail.core import hooks
-from wagtail.core.models import Orderable, Page
-from modelcluster.fields import ParentalKey
 
 from coderedcms.api.mailchimp import MailchimpApi
 
@@ -26,7 +24,8 @@ class MailchimpSubscriberIntegrationWidget(Input):
         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']['selectable_mailchimp_lists'] = self.get_selectable_mailchimp_lists(
+            list_library)
         ctx['widget']['stored_mailchimp_list'] = self.get_stored_mailchimp_list(json_value)
 
         return ctx
@@ -34,15 +33,14 @@ class MailchimpSubscriberIntegrationWidget(Input):
     def render_js(self, name, list_library, json_value):
         ctx = {
             'widget_name': name,
-            'widget_js_name' : name.replace('-', '_'),
-            'list_library' : list_library,
+            '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),
+            'stored_merge_fields': self.get_stored_merge_fields(json_value),
         }
 
         return render_to_string(self.js_template_name, ctx)
 
-
     def get_json_value(self, value):
         if value:
             json_value = json.loads(value)
@@ -85,18 +83,22 @@ class MailchimpSubscriberIntegrationWidget(Input):
                     'merge_fields': {},
                     'interest_categories': {}
                 }
-                
-                list_library[l['id']]['merge_fields'] = mailchimp.get_merge_fields_for_list(l['id'])['merge_fields']
-                list_library[l['id']]['interest_categories'] = mailchimp.get_interest_categories_for_list(l['id'])['categories']
+
+                list_library[l['id']]['merge_fields'] = mailchimp.get_merge_fields_for_list(l['id'])['merge_fields']  # noqa
+                list_library[l['id']]['interest_categories'] = mailchimp.get_interest_categories_for_list(l['id'])['categories']  # noqa
 
                 for category in list_library[l['id']]['interest_categories']:
-                    category['interests'] = mailchimp.get_interests_for_interest_category(l['id'], category['id'])['interests']
+                    category['interests'] = mailchimp.get_interests_for_interest_category(
+                        l['id'],
+                        category['id']
+                    )['interests']
 
         return list_library
 
+
 class MailchimpSubscriberIntegration(models.Model):
     class Meta:
-        abstract=True
+        abstract = True
 
     subscriber_json_data = models.TextField(
         blank=True,
@@ -106,7 +108,8 @@ class MailchimpSubscriberIntegration(models.Model):
     def integration_operation(self, instance, **kwargs):
         mailchimp = MailchimpApi()
         if mailchimp.is_active:
-            rendered_dictionary = self.render_dictionary(self.format_form_submission(kwargs['form_submission']))
+            rendered_dictionary = self.render_dictionary(
+                self.format_form_submission(kwargs['form_submission']))
             mailchimp.add_user_to_list(list_id=self.get_list_id(), data=rendered_dictionary)
 
     def format_form_submission(self, form_submission):
@@ -146,14 +149,15 @@ class MailchimpSubscriberIntegration(models.Model):
             ]
         })
 
-
-        rendered_dictionary = Template(rendered_dictionary_template).render(Context(form_submission))
+        rendered_dictionary = Template(
+            rendered_dictionary_template).render(Context(form_submission))
         return rendered_dictionary
 
     panels = [
         FieldPanel('subscriber_json_data', widget=MailchimpSubscriberIntegrationWidget)
     ]
 
+
 @hooks.register('form_page_submit')
 def run_mailchimp_subscriber_integrations(instance, **kwargs):
     if hasattr(instance, 'integration_panels'):

+ 114 - 73
coderedcms/models/page_models.py

@@ -12,7 +12,7 @@ from django.contrib import messages
 from django.core.files.uploadedfile import InMemoryUploadedFile, TemporaryUploadedFile
 from django.core.files.storage import FileSystemStorage
 from django.core.mail import EmailMessage
-from django.core.paginator import Paginator
+from django.core.paginator import Paginator, InvalidPage, EmptyPage, PageNotAnInteger
 from django.core.serializers.json import DjangoJSONEncoder
 from django.core.validators import MaxValueValidator, MinValueValidator
 from django.db import models
@@ -41,7 +41,8 @@ from wagtail.admin.edit_handlers import (
     ObjectList,
     PageChooserPanel,
     StreamFieldPanel,
-    TabbedInterface)
+    TabbedInterface
+)
 from wagtail.core import hooks
 from wagtail.core.fields import StreamField
 from wagtail.core.models import Orderable, PageBase, Page, Site
@@ -60,13 +61,21 @@ from coderedcms.blocks import (
     STREAMFORM_BLOCKS,
     ContentWallBlock,
     OpenHoursBlock,
-    StructuredDataActionBlock)
+    StructuredDataActionBlock
+)
 from coderedcms.fields import ColorField
 from coderedcms.forms import CoderedFormBuilder, CoderedSubmissionsListView
 from coderedcms.models.snippet_models import ClassifierTerm
-from coderedcms.models.wagtailsettings_models import GeneralSettings, LayoutSettings, SeoSettings, GoogleApiSettings
+from coderedcms.models.wagtailsettings_models import GeneralSettings, LayoutSettings, SeoSettings, GoogleApiSettings  # noqa
 from coderedcms.wagtail_flexible_forms.blocks import FormFieldBlock, FormStepBlock
-from coderedcms.wagtail_flexible_forms.models import Step, Steps, StreamFormMixin, StreamFormJSONEncoder, SessionFormSubmission, SubmissionRevision
+from coderedcms.wagtail_flexible_forms.models import (
+    Step,
+    Steps,
+    StreamFormMixin,
+    StreamFormJSONEncoder,
+    SessionFormSubmission,
+    SubmissionRevision
+)
 from coderedcms.settings import cr_settings
 from coderedcms.widgets import ClassifierSelectWidget
 
@@ -76,6 +85,7 @@ logger = logging.getLogger('coderedcms')
 
 CODERED_PAGE_MODELS = []
 
+
 def get_page_models():
     return CODERED_PAGE_MODELS
 
@@ -100,11 +110,13 @@ class CoderedPageMeta(PageBase):
         if not cls._meta.abstract:
             CODERED_PAGE_MODELS.append(cls)
 
+
 class CoderedTag(TaggedItemBase):
     class Meta:
         verbose_name = _('CodeRed Tag')
     content_object = ParentalKey('coderedcms.CoderedPage', related_name='tagged_items')
 
+
 class CoderedPage(WagtailCacheMixin, Page, metaclass=CoderedPageMeta):
     """
     General use page with caching, templating, and SEO functionality.
@@ -124,7 +136,6 @@ class CoderedPage(WagtailCacheMixin, Page, metaclass=CoderedPageMeta):
     # ajax_template = ''
     # search_template = ''
 
-
     ###############
     # Content fields
     ###############
@@ -138,7 +149,6 @@ class CoderedPage(WagtailCacheMixin, Page, metaclass=CoderedPageMeta):
         verbose_name=_('Cover image'),
     )
 
-
     ###############
     # Index fields
     ###############
@@ -184,7 +194,6 @@ class CoderedPage(WagtailCacheMixin, Page, metaclass=CoderedPageMeta):
         help_text=_('Enable filtering child pages by these classifiers.'),
     )
 
-
     ###############
     # Layout fields
     ###############
@@ -196,7 +205,6 @@ class CoderedPage(WagtailCacheMixin, Page, metaclass=CoderedPageMeta):
         verbose_name=_('Template')
     )
 
-
     ###############
     # SEO fields
     ###############
@@ -208,7 +216,9 @@ class CoderedPage(WagtailCacheMixin, Page, metaclass=CoderedPageMeta):
         on_delete=models.SET_NULL,
         related_name='+',
         verbose_name=_('Open Graph preview image'),
-        help_text=_('The image shown when linking to this page on social media. If blank, defaults to article cover image, or logo in Settings > Layout > Logo')
+        help_text=_(
+            "The image shown when linking to this page on social media. If blank, defaults to article cover image, or logo in Settings > Layout > Logo"  # noqa
+        )
     )
     struct_org_type = models.CharField(
         default='',
@@ -241,7 +251,9 @@ class CoderedPage(WagtailCacheMixin, Page, metaclass=CoderedPageMeta):
         on_delete=models.SET_NULL,
         related_name='+',
         verbose_name=_('Photo of Organization'),
-        help_text=_('A photo of the facility. This photo will be cropped to 1:1, 4:3, and 16:9 aspect ratios automatically.')
+        help_text=_(
+            "A photo of the facility. This photo will be cropped to 1:1, 4:3, and 16:9 aspect ratios automatically."  # noqa
+        )
     )
     struct_org_phone = models.CharField(
         blank=True,
@@ -277,7 +289,9 @@ class CoderedPage(WagtailCacheMixin, Page, metaclass=CoderedPageMeta):
         blank=True,
         max_length=255,
         verbose_name=_('Country'),
-        help_text=_('For example, USA. Two-letter ISO 3166-1 alpha-2 country code is also acceptible https://en.wikipedia.org/wiki/ISO_3166-1')
+        help_text=_(
+            "For example, USA. Two-letter ISO 3166-1 alpha-2 country code is also acceptible https://en.wikipedia.org/wiki/ISO_3166-1"  # noqa
+        )
     )
     struct_org_geo_lat = models.DecimalField(
         blank=True,
@@ -310,10 +324,11 @@ class CoderedPage(WagtailCacheMixin, Page, metaclass=CoderedPageMeta):
     struct_org_extra_json = models.TextField(
         blank=True,
         verbose_name=_('Additional Organization markup'),
-        help_text=_('Additional JSON-LD inserted into the Organization dictionary. Must be properties of https://schema.org/Organization or the selected organization type.')
+        help_text=_(
+            "Additional JSON-LD inserted into the Organization dictionary. Must be properties of https://schema.org/Organization or the selected organization type."  # noqa
+        )
     )
 
-
     ###############
     # Classify
     ###############
@@ -322,7 +337,9 @@ class CoderedPage(WagtailCacheMixin, Page, metaclass=CoderedPageMeta):
         'coderedcms.ClassifierTerm',
         blank=True,
         verbose_name=_('Classifiers'),
-        help_text=_('Categorize and group pages together with classifiers. Used to organize and filter pages across the site.'),
+        help_text=_(
+            "Categorize and group pages together with classifiers. Used to organize and filter pages across the site."  # noqa
+        ),
     )
     tags = ClusterTaggableManager(
         through=CoderedTag,
@@ -331,7 +348,6 @@ class CoderedPage(WagtailCacheMixin, Page, metaclass=CoderedPageMeta):
         help_text=_('Used to organize pages across the site.'),
     )
 
-
     ###############
     # Settings
     ###############
@@ -344,7 +360,6 @@ class CoderedPage(WagtailCacheMixin, Page, metaclass=CoderedPageMeta):
         verbose_name=_('Content Walls')
     )
 
-
     ###############
     # Search
     ###############
@@ -370,7 +385,6 @@ class CoderedPage(WagtailCacheMixin, Page, metaclass=CoderedPageMeta):
         index.FilterField('classifier_terms'),
     ]
 
-
     ###############
     # Panels
     ###############
@@ -460,8 +474,7 @@ class CoderedPage(WagtailCacheMixin, Page, metaclass=CoderedPageMeta):
         super().__init__(*args, **kwargs)
         klassname = self.__class__.__name__.lower()
         template_choices = cr_settings['FRONTEND_TEMPLATES_PAGES'].get('*', ()) + \
-                           cr_settings['FRONTEND_TEMPLATES_PAGES'].get(klassname, ())
-
+            cr_settings['FRONTEND_TEMPLATES_PAGES'].get(klassname, ())
 
         self._meta.get_field('index_order_by').choices = self.index_order_by_choices
         self._meta.get_field('custom_template').choices = template_choices
@@ -469,14 +482,16 @@ class CoderedPage(WagtailCacheMixin, Page, metaclass=CoderedPageMeta):
             self.index_order_by = self.index_order_by_default
             self.index_show_subpages = self.index_show_subpages_default
 
-
     @classmethod
     def get_edit_handler(cls):
         """
         Override to "lazy load" the panels overriden by subclasses.
         """
         panels = [
-            ObjectList(cls.content_panels + cls.body_content_panels + cls.bottom_content_panels, heading=_('Content')),
+            ObjectList(
+                cls.content_panels + cls.body_content_panels + cls.bottom_content_panels,
+                heading=_('Content')
+            ),
             ObjectList(cls.classify_panels, heading=_('Classify')),
             ObjectList(cls.layout_panels, heading=_('Layout')),
             ObjectList(cls.promote_panels, heading=_('SEO'), classname="seo"),
@@ -484,7 +499,11 @@ class CoderedPage(WagtailCacheMixin, Page, metaclass=CoderedPageMeta):
         ]
 
         if cls.integration_panels:
-            panels.append(ObjectList(cls.integration_panels, heading='Integrations', classname='integrations'))
+            panels.append(ObjectList(
+                cls.integration_panels,
+                heading='Integrations',
+                classname='integrations'
+            ))
 
         return TabbedInterface(panels).bind_to_model(cls)
 
@@ -580,13 +599,18 @@ class CoderedPage(WagtailCacheMixin, Page, metaclass=CoderedPageMeta):
                         try:
                             for term in selected_terms:
                                 all_children = all_children.filter(classifier_terms=term)
-                        except:
-                            logger.warning("Tried to filter by ClassifierTerm, but <%s.%s ('%s')>.get_index_children() did not return a queryset or is not a queryset of CoderedPage models.", self._meta.app_label, self.__class__.__name__, self.title)
+                        except AttributeError:
+                            logger.warning(
+                                "Tried to filter by ClassifierTerm, but <%s.%s ('%s')>.get_index_children() did not return a queryset or is not a queryset of CoderedPage models.",  # noqa
+                                self._meta.app_label,
+                                self.__class__.__name__,
+                                self.title
+                            )
             paginator = Paginator(all_children, self.index_num_per_page)
             pagenum = request.GET.get('p', 1)
             try:
                 paged_children = paginator.page(pagenum)
-            except:
+            except (PageNotAnInteger, EmptyPage, InvalidPage) as e:  # noqa
                 paged_children = paginator.page(1)
 
             context['index_paginated'] = paged_children
@@ -594,9 +618,6 @@ class CoderedPage(WagtailCacheMixin, Page, metaclass=CoderedPageMeta):
         context['content_walls'] = self.get_content_walls(check_child_setting=False)
         return context
 
-
-
-
 ###############################################################################
 # Abstract pages providing pre-built common website functionality, suitable for subclassing.
 # These are abstract so subclasses can override fields if desired.
@@ -841,10 +862,11 @@ class CoderedEventPage(CoderedWebPage, BaseEvent):
             for occurrence in self.occurrences.all():
                 aoc += [instance for instance in occurrence.all_occurrences()]
             if len(aoc) > 0:
-                return aoc[-1] # last one in the list
+                return aoc[-1]  # last one in the list
 
         except AttributeError:
-            # Triggers when a preview is initiated on an EventPage because it uses a FakeQuerySet object.
+            # Triggers when a preview is initiated on an
+            # EventPage because it uses a FakeQuerySet object.
             # Here we manually compute the next_occurrence
             occurrences = [e.next_occurrence() for e in self.occurrences.all()]
             if occurrences:
@@ -862,21 +884,23 @@ class CoderedEventPage(CoderedWebPage, BaseEvent):
         }
 
         if 'limit' in kwargs:
-            if kwargs['limit'] != None:
-                # Limit the number of event instances that will be generated per occurrence rule to 10, if not otherwise specified.
+            if kwargs['limit'] is not None:
+                # Limit the number of event instances that will be
+                # generated per occurrence rule to 10, if not otherwise specified.
                 occurrence_kwargs['limit'] = kwargs.get('limit', 10)
 
         # For each occurrence rule in all of the occurrence rules for this event.
         for occurrence in self.occurrences.all():
 
             # Add the qualifying generated event instances to the list.
-            event_instances += [instance for instance in occurrence.all_occurrences(**occurrence_kwargs)]
+            event_instances += [
+                instance for instance in occurrence.all_occurrences(**occurrence_kwargs)]
 
         # Sort all the events by the date that they start
         event_instances.sort(key=lambda d: d[0])
 
         # Return the event instances, possibly spliced if num_instances_to_return is set.
-        return event_instances[:num_of_instances_to_return] if num_of_instances_to_return else event_instances
+        return event_instances[:num_of_instances_to_return] if num_of_instances_to_return else event_instances  # noqa
 
     def convert_to_ical_format(self, dt_start=None, dt_end=None, occurrence=None):
         ical_event = ICalEvent()
@@ -892,7 +916,8 @@ class CoderedEventPage(CoderedWebPage, BaseEvent):
 
         if occurrence:
             freq = occurrence.repeat.split(":")[1] if occurrence.repeat else None
-            repeat_until = occurrence.repeat_until.strftime("%Y%m%dT000000Z") if occurrence.repeat_until else None
+            repeat_until = occurrence.repeat_until.strftime(
+                "%Y%m%dT000000Z") if occurrence.repeat_until else None
 
             ical_event.add('dtstart', occurrence.start)
 
@@ -916,6 +941,7 @@ class CoderedEventPage(CoderedWebPage, BaseEvent):
             events.append(self.convert_to_ical_format(occurrence=occurrence))
         return events
 
+
 class DefaultCalendarViewChoices():
     MONTH = 'month'
     AGENDA_WEEK = 'agendaWeek'
@@ -930,6 +956,7 @@ class DefaultCalendarViewChoices():
         (LIST_MONTH, _('Calendar List View')),
     )
 
+
 class CoderedEventIndexPage(CoderedWebPage):
     """
     Shows a list of event sub-pages.
@@ -984,7 +1011,7 @@ class CoderedEventIndexPage(CoderedWebPage):
                 event_data = {
                     'title': event.title,
                     'start': occurrence[0].strftime('%Y-%m-%dT%H:%M:%S'),
-                    'end' : occurrence[1].strftime('%Y-%m-%dT%H:%M:%S') if occurrence[1] else "",
+                    'end': occurrence[1].strftime('%Y-%m-%dT%H:%M:%S') if occurrence[1] else "",
                     'description': "",
                 }
                 if event.url:
@@ -1002,25 +1029,28 @@ class CoderedEventOccurrence(Orderable, BaseOccurrence):
 
 
 class CoderedFormMixin(models.Model):
-
     class Meta:
         abstract = True
 
     submissions_list_view_class = CoderedSubmissionsListView
     encoder = DjangoJSONEncoder
 
-    ### Custom codered fields
+    # Custom codered fields
     to_address = models.CharField(
         max_length=255,
         blank=True,
         verbose_name=_('Email form submissions to'),
-        help_text=_('Optional - email form submissions to this address. Separate multiple addresses by comma.')
+        help_text=_(
+            "Optional - email form submissions to this address. Separate multiple addresses by comma."  # noqa
+        )
     )
     reply_address = models.CharField(
         max_length=255,
         blank=True,
         verbose_name=_('Reply-to address'),
-        help_text=_('Optional - to reply to the submitter, specify the email field here. For example, if a form field above is labeled "Your Email", enter: {{ your_email }}')
+        help_text=_(
+            "Optional - to reply to the submitter, specify the email field here. For example, if a form field above is labeled 'Your Email', enter: {{ your_email }}"  # noqa
+        )
     )
     subject = models.CharField(
         max_length=255,
@@ -1136,7 +1166,6 @@ class CoderedFormMixin(models.Model):
         FieldPanel('spam_protection')
     ]
 
-
     @property
     def form_live(self):
         """
@@ -1174,9 +1203,9 @@ class CoderedFormMixin(models.Model):
 
     def get_storage(self):
         return FileSystemStorage(
-                location=cr_settings['PROTECTED_MEDIA_ROOT'],
-                base_url=cr_settings['PROTECTED_MEDIA_URL']
-            )
+            location=cr_settings['PROTECTED_MEDIA_ROOT'],
+            base_url=cr_settings['PROTECTED_MEDIA_URL']
+        )
 
     def process_form_submission(self, request, form, form_submission, processed_data):
 
@@ -1412,13 +1441,13 @@ class CoderedFormPage(CoderedFormMixin, CoderedWebPage):
     form_builder = CoderedFormBuilder
 
     body_content_panels = [
-            InlinePanel('form_fields', label="Form fields"),
-        ] + \
+        InlinePanel('form_fields', label="Form fields"),
+    ] + \
         CoderedWebPage.body_content_panels + \
         CoderedFormMixin.body_content_panels + [
             FormSubmissionsPanel(),
             InlinePanel('confirmation_emails', label=_('Confirmation Emails'))
-        ]
+    ]
 
     settings_panels = CoderedPage.settings_panels + CoderedFormMixin.settings_panels
 
@@ -1494,9 +1523,9 @@ class CoderedSessionFormSubmission(SessionFormSubmission):
         if 'user' in submission_data:
             submission_data['user'] = str(submission_data['user'])
         submission = FormSubmission.objects.create(
-                form_data=json.dumps(submission_data, cls=StreamFormJSONEncoder),
-                page=self.page
-            )
+            form_data=json.dumps(submission_data, cls=StreamFormJSONEncoder),
+            page=self.page
+        )
 
         if delete_self:
             CoderedSubmissionRevision.objects.filter(submission_id=self.id).delete()
@@ -1510,11 +1539,9 @@ class CoderedSessionFormSubmission(SessionFormSubmission):
     def render_link(self, value):
         return "{0}{1}".format(cr_settings['PROTECTED_MEDIA_URL'], value)
 
-
     def render_image(self, value):
         return "{0}{1}".format(cr_settings['PROTECTED_MEDIA_URL'], value)
 
-
     def render_file(self, value):
         return "{0}{1}".format(cr_settings['PROTECTED_MEDIA_URL'], value)
 
@@ -1526,8 +1553,7 @@ def create_submission_changed_revision(sender, **kwargs):
     submission = kwargs['instance']
     created = kwargs['created']
     CoderedSubmissionRevision.create_from_submission(
-        submission, (CoderedSubmissionRevision.CREATED if created
-                     else CoderedSubmissionRevision.CHANGED))
+        submission, (CoderedSubmissionRevision.CREATED if created else CoderedSubmissionRevision.CHANGED))  # noqa
 
 
 @receiver(post_delete)
@@ -1535,8 +1561,7 @@ def create_submission_deleted_revision(sender, **kwargs):
     if not issubclass(sender, CoderedSessionFormSubmission):
         return
     submission = kwargs['instance']
-    CoderedSubmissionRevision.create_from_submission(submission,
-                                              SubmissionRevision.DELETED)
+    CoderedSubmissionRevision.create_from_submission(submission, SubmissionRevision.DELETED)  # noqa
 
 
 class CoderedStep(Step):
@@ -1571,7 +1596,7 @@ class CoderedSteps(Steps):
 
 class CoderedStreamFormMixin(StreamFormMixin):
     class Meta:
-        abstract=True
+        abstract = True
 
     def get_steps(self, request=None):
         if not hasattr(self, 'steps'):
@@ -1625,8 +1650,8 @@ class CoderedStreamFormPage(CoderedFormMixin, CoderedStreamFormMixin, CoderedWeb
     body_content_panels = [
         StreamFieldPanel('form_fields')
     ] + \
-    CoderedFormMixin.body_content_panels + [
-        InlinePanel('confirmation_emails', label=_('Confirmation Emails'))
+        CoderedFormMixin.body_content_panels + [
+            InlinePanel('confirmation_emails', label=_('Confirmation Emails'))
     ]
 
     def process_form_post(self, form, request):
@@ -1653,9 +1678,10 @@ class CoderedStreamFormPage(CoderedFormMixin, CoderedStreamFormMixin, CoderedWeb
 
     def get_storage(self):
         return FileSystemStorage(
-                location=cr_settings['PROTECTED_MEDIA_ROOT'],
-                base_url=cr_settings['PROTECTED_MEDIA_URL']
-            )
+            location=cr_settings['PROTECTED_MEDIA_ROOT'],
+            base_url=cr_settings['PROTECTED_MEDIA_URL']
+        )
+
 
 class CoderedLocationPage(CoderedWebPage):
     """
@@ -1687,7 +1713,8 @@ class CoderedLocationPage(CoderedWebPage):
     auto_update_latlng = models.BooleanField(
         default=True,
         verbose_name=_("Auto Update Latitude and Longitude"),
-        help_text=_("If checked, automatically update the latitude and longitude when the address is updated.")
+        help_text=_(
+            "If checked, automatically update the latitude and longitude when the address is updated.")  # noqa
     )
     map_title = models.CharField(
         blank=True,
@@ -1767,20 +1794,24 @@ class CoderedLocationPage(CoderedWebPage):
     def to_geojson(self):
         return {
             "type": "Feature",
-            "geometry":{
+            "geometry": {
                 "type": "Point",
                 "coordinates": [self.longitude, self.latitude]
             },
-            "properties":{
+            "properties": {
                 "list_description": self.render_list_description,
                 "pin_description": self.render_pin_description
             }
         }
 
     def save(self, *args, **kwargs):
-        if self.auto_update_latlng and GoogleApiSettings.for_site(Site.objects.get(is_default_site=True)).google_maps_api_key:
+        if self.auto_update_latlng and GoogleApiSettings.for_site(
+            Site.objects.get(is_default_site=True)
+        ).google_maps_api_key:
             try:
-                g = geocoder.google(self.address, key=GoogleApiSettings.for_site(Site.objects.get(is_default_site=True)).google_maps_api_key)
+                g = geocoder.google(self.address, key=GoogleApiSettings.for_site(
+                    Site.objects.get(is_default_site=True)
+                ).google_maps_api_key)
                 self.latitude = g.latlng[0]
                 self.longitude = g.latlng[1]
             except TypeError:
@@ -1789,10 +1820,11 @@ class CoderedLocationPage(CoderedWebPage):
 
         return super(CoderedLocationPage, self).save(*args, **kwargs)
 
-
     def get_context(self, request, *args, **kwargs):
         context = super().get_context(request)
-        context['google_api_key'] = GoogleApiSettings.for_site(Site.objects.get(is_default_site=True)).google_maps_api_key
+        context['google_api_key'] = GoogleApiSettings.for_site(
+            Site.objects.get(is_default_site=True)
+        ).google_maps_api_key
         return context
 
 
@@ -1826,7 +1858,9 @@ class CoderedLocationIndexPage(CoderedWebPage):
             MaxValueValidator(20),
             MinValueValidator(1),
         ],
-        help_text=_('Requires API key to use zoom. 1: World, 5: Landmass/continent, 10: City, 15: Streets, 20: Buildings')
+        help_text=_(
+            "Requires API key to use zoom. 1: World, 5: Landmass/continent, 10: City, 15: Streets, 20: Buildings"  # noqa
+        )
     )
 
     layout_panels = CoderedWebPage.layout_panels + [
@@ -1858,7 +1892,12 @@ class CoderedLocationIndexPage(CoderedWebPage):
             southwest = [float(x) for x in southwest.split(',')]
             northeast = [float(x) for x in northeast.split(',')]
 
-            qs = qs.filter(latitude__gte=southwest[0], latitude__lte=northeast[0], longitude__gte=southwest[1], longitude__lte=northeast[1])
+            qs = qs.filter(
+                latitude__gte=southwest[0],
+                latitude__lte=northeast[0],
+                longitude__gte=southwest[1],
+                longitude__lte=northeast[1]
+            )
 
         return {
             "type": "FeatureCollection",
@@ -1879,5 +1918,7 @@ class CoderedLocationIndexPage(CoderedWebPage):
 
     def get_context(self, request, *args, **kwargs):
         context = super().get_context(request)
-        context['google_api_key'] = GoogleApiSettings.for_site(Site.objects.get(is_default_site=True)).google_maps_api_key
+        context['google_api_key'] = GoogleApiSettings.for_site(
+            Site.objects.get(is_default_site=True)
+        ).google_maps_api_key
         return context

+ 36 - 7
coderedcms/models/snippet_models.py

@@ -38,12 +38,14 @@ class Carousel(ClusterableModel):
     show_controls = models.BooleanField(
         default=True,
         verbose_name=_('Show controls'),
-        help_text=_('Shows arrows on the left and right of the carousel to advance next or previous slides.'),
+        help_text=_(
+            "Shows arrows on the left and right of the carousel to advance next or previous slides."  # noqa
+        ),
     )
     show_indicators = models.BooleanField(
         default=True,
         verbose_name=_('Show indicators'),
-        help_text=_('Shows small indicators at the bottom of the carousel based on the number of slides.'),
+        help_text=_('Shows small indicators at the bottom of the carousel based on the number of slides.'),  # noqa
     )
     animation = models.CharField(
         blank=True,
@@ -297,6 +299,7 @@ class Footer(models.Model):
     def __str__(self):
         return self.name
 
+
 @register_snippet
 class ReusableContent(models.Model):
     """
@@ -375,11 +378,37 @@ class CoderedEmail(ClusterableModel):
         abstract = True
         verbose_name = _('CodeRed Email')
 
-    to_address = models.CharField(max_length=255, blank=True, verbose_name=_('To Addresses'), help_text=_('Separate multiple email addresses with commas.'))
-    from_address = models.CharField(max_length=255, blank=True, verbose_name=_('From Address'), help_text=_('For example: "sender@example.com" or "Sender Name <sender@example.com>" (without quotes).'))
-    reply_address = models.CharField(max_length=255, blank=True, verbose_name=_('Reply-To Address'), help_text=_('Separate multiple email addresses with commas.'))
-    cc_address = models.CharField(max_length=255, blank=True, verbose_name=_('CC'), help_text=_('Separate multiple email addresses with commas.'))
-    bcc_address = models.CharField(max_length=255, blank=True, verbose_name=_('BCC'), help_text=_('Separate multiple email addresses with commas.'))
+    to_address = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name=_('To Addresses'),
+        help_text=_('Separate multiple email addresses with commas.')
+    )
+    from_address = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name=_('From Address'),
+        help_text=_(
+            'For example: "sender@example.com" or "Sender Name <sender@example.com>" (without quotes).')  # noqa
+    )
+    reply_address = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name=_('Reply-To Address'),
+        help_text=_('Separate multiple email addresses with commas.')
+    )
+    cc_address = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name=_('CC'),
+        help_text=_('Separate multiple email addresses with commas.')
+    )
+    bcc_address = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name=_('BCC'),
+        help_text=_('Separate multiple email addresses with commas.')
+    )
     subject = models.CharField(max_length=255, blank=True, verbose_name=_('Subject'))
     body = models.TextField(blank=True, verbose_name=_('Body'))
 

+ 4 - 3
coderedcms/models/tests/test_page_models.py

@@ -26,6 +26,7 @@ from coderedcms.tests.testapp.models import (
     WebPage
 )
 
+
 class BasicPageTestCase():
     """
     This is a testing mixin used to run common tests for basic versions of page types.
@@ -59,7 +60,7 @@ class AbstractPageTestCase():
 
     def test_not_available(self):
         """
-        Tests to make sure the page is not creatable and not in CodeRed CMS's global list of page models.
+        Tests to make sure the page is not creatable and not in CodeRed CMS's global list of page models.  # noqa
         """
         self.assertFalse(self.model.is_creatable)
         self.assertFalse(self.model in get_page_models())
@@ -100,7 +101,7 @@ class ConcreteFormPageTestCase(ConcreteBasicPageTestCase):
         """
         Test to check if the default spam catching works.
         """
-        response = self.client.post(self.basic_page.url, {'cr-decoy-comments': 'This is Spam'}, follow=True)
+        response = self.client.post(self.basic_page.url, {'cr-decoy-comments': 'This is Spam'}, follow=True)  # noqa
         messages = list(response.context['messages'])
         self.assertEqual(len(messages), 1)
         self.assertEqual(str(messages[0]), self.basic_page.get_spam_message())
@@ -190,4 +191,4 @@ class LocationPageTestCase(ConcreteBasicPageTestCase, WagtailPageTests):
 
 
 class StreamFormPageTestCase(ConcreteFormPageTestCase, WagtailPageTests):
-    model = StreamFormPage
+    model = StreamFormPage

+ 25 - 9
coderedcms/models/wagtailsettings_models.py

@@ -123,7 +123,7 @@ class LayoutSettings(BaseSetting):
         choices=cr_settings['FRONTEND_NAVBAR_COLOR_SCHEME_CHOICES'],
         default=cr_settings['FRONTEND_NAVBAR_COLOR_SCHEME_DEFAULT'],
         verbose_name=_('Navbar color scheme'),
-        help_text=_('Optimizes text and other navbar elements for use with light or dark backgrounds.'),
+        help_text=_('Optimizes text and other navbar elements for use with light or dark backgrounds.'),  # noqa
     )
     navbar_class = models.CharField(
         blank=True,
@@ -223,7 +223,9 @@ class AnalyticsSettings(BaseSetting):
     ga_track_button_clicks = models.BooleanField(
         default=False,
         verbose_name=_('Track button clicks'),
-        help_text=_('Track all button clicks using Google Analytics event tracking. Event tracking details can be specified in each button’s advanced settings options.'),
+        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
+        ),
     )
 
     panels = [
@@ -248,7 +250,9 @@ class ADASettings(BaseSetting):
     skip_navigation = models.BooleanField(
         default=False,
         verbose_name=_('Show skip navigation link'),
-        help_text=_('Shows a "Skip Navigation" link above the navbar that takes you directly to the main content.'),
+        help_text=_(
+            "Shows a 'Skip Navigation' link above the navbar that takes you directly to the main content."  # noqa
+        ),
     )
 
     panels = [
@@ -273,7 +277,9 @@ class GeneralSettings(BaseSetting):
         blank=True,
         max_length=255,
         verbose_name=_('From email address'),
-        help_text=_('The default email address this site appears to send from. For example: "sender@example.com" or "Sender Name <sender@example.com>" (without quotes)'),
+        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
+        ),
     )
     search_num_results = models.PositiveIntegerField(
         default=10,
@@ -311,22 +317,30 @@ class SeoSettings(BaseSetting):
     og_meta = models.BooleanField(
         default=True,
         verbose_name=_('Use OpenGraph Markup'),
-        help_text=_('Show an optimized preview when linking to this site on Facebook, Linkedin, Twitter, and others. See http://ogp.me/.'),
+        help_text=_(
+            "Show an optimized preview when linking to this site on Facebook, Linkedin, Twitter, and others. See http://ogp.me/."  # noqa
+        ),
     )
     twitter_meta = models.BooleanField(
         default=True,
         verbose_name=_('Use Twitter Markup'),
-        help_text=_('Shows content as a "card" when linking to this site on Twitter. See https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/abouts-cards.'),
+        help_text=_(
+            "Shows content as a 'card' when linking to this site on Twitter. See https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/abouts-cards."  # noqa
+        ),
     )
     struct_meta = models.BooleanField(
         default=True,
         verbose_name=_('Use Structured Data'),
-        help_text=_('Optimizes information about your organization for search engines. See https://schema.org/.'),
+        help_text=_(
+            "Optimizes information about your organization for search engines. See https://schema.org/."  # noqa
+        ),
     )
     amp_pages = models.BooleanField(
         default=True,
         verbose_name=_('Use AMP Pages'),
-        help_text=_('Generates an alternate AMP version of Article pages that are preferred by search engines. See https://www.ampproject.org/'),
+        help_text=_(
+            "Generates an alternate AMP version of Article pages that are preferred by search engines. See https://www.ampproject.org/"  # noqa
+        ),
     )
 
     panels = [
@@ -336,7 +350,9 @@ class SeoSettings(BaseSetting):
                 FieldPanel('twitter_meta'),
                 FieldPanel('struct_meta'),
                 FieldPanel('amp_pages'),
-                HelpPanel(content=_('If these settings are enabled, the corresponding values in each page’s SEO tab are used.')),
+                HelpPanel(content=_(
+                    "If these settings are enabled, the corresponding values in each page’s SEO tab are used."  # noqa
+                )),
             ],
             heading=_('Search Engine Optimization')
         )

+ 2 - 2
coderedcms/project_template/project_name/settings/dev.py

@@ -1,4 +1,4 @@
-from .base import *
+from .base import *  # noqa
 
 # SECURITY WARNING: don't run with debug turned on in production!
 DEBUG = True
@@ -13,6 +13,6 @@ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
 WAGTAIL_CACHE = False
 
 try:
-    from .local_settings import *
+    from .local_settings import *  # noqa
 except ImportError:
     pass

+ 3 - 3
coderedcms/project_template/project_name/settings/prod.py

@@ -1,4 +1,4 @@
-from .base import *
+from .base import *  # noqa
 
 # SECURITY WARNING: don't run with debug turned on in production!
 DEBUG = False
@@ -67,13 +67,13 @@ TEMPLATES = [
 CACHES = {
     'default': {
         'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
-        'LOCATION': os.path.join(BASE_DIR, 'cache'),
+        'LOCATION': os.path.join(BASE_DIR, 'cache'),  # noqa
         'KEY_PREFIX': 'coderedcms',
         'TIMEOUT': 14400,  # in seconds
     }
 }
 
 try:
-    from .local_settings import *
+    from .local_settings import *  # noqa
 except ImportError:
     pass

+ 1 - 1
coderedcms/project_template/project_name/urls.py

@@ -24,7 +24,7 @@ urlpatterns = [
 
     # Alternatively, if you want CMS pages to be served from a subpath
     # of your site, rather than the site root:
-    #    re_path(r'^pages/', include(codered_urls)),
+    #    re_path(r"^pages/", include(codered_urls)),
 ]
 
 

+ 11 - 7
coderedcms/settings.py

@@ -3,14 +3,16 @@ from django.conf import settings
 from django.utils.lru_cache import lru_cache
 
 
-PROJECT_DIR = settings.PROJECT_DIR if getattr(settings, 'PROJECT_DIR') else os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+PROJECT_DIR = settings.PROJECT_DIR if getattr(settings, 'PROJECT_DIR') else os.path.dirname(
+    os.path.dirname(os.path.abspath(__file__))
+)
 BASE_DIR = settings.BASE_DIR if getattr(settings, 'BASE_DIR') else os.path.dirname(PROJECT_DIR)
 
 DEFAULTS = {
     'PROTECTED_MEDIA_URL': '/protected/',
     'PROTECTED_MEDIA_ROOT': os.path.join(BASE_DIR, 'protected'),
     'PROTECTED_MEDIA_UPLOAD_WHITELIST': [],
-    'PROTECTED_MEDIA_UPLOAD_BLACKLIST': ['.sh', '.exe', '.bat', '.ps1', '.app', '.jar', '.py', '.php', '.pl', '.rb'],
+    'PROTECTED_MEDIA_UPLOAD_BLACKLIST': ['.sh', '.exe', '.bat', '.ps1', '.app', '.jar', '.py', '.php', '.pl', '.rb'],  # noqa
 
     'FRONTEND_BTN_SIZE_DEFAULT': '',
     'FRONTEND_BTN_SIZE_CHOICES': (
@@ -95,7 +97,7 @@ DEFAULTS = {
         ('navbar-expand-xl', 'xl - Show on extra large screens (desktop, wide monitor)'),
     ),
 
-    'FRONTEND_THEME_HELP': 'Change the color palette of your site with a Bootstrap theme. Powered by Bootswatch https://bootswatch.com/.',
+    'FRONTEND_THEME_HELP': "Change the color palette of your site with a Bootstrap theme. Powered by Bootswatch https://bootswatch.com/.",  # noqa
     'FRONTEND_THEME_DEFAULT': '',
     'FRONTEND_THEME_CHOICES': (
         ('', 'Default - Classic Bootstrap'),
@@ -140,9 +142,12 @@ DEFAULTS = {
             ('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'),
+            ('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'),
@@ -163,7 +168,6 @@ DEFAULTS = {
             ('coderedcms/pages/home_page.html', 'Home page without title and cover image'),
             ('coderedcms/pages/base.html', 'Blank page - no navbar or footer'),
         ),
-
     },
 
     'BANNER': None,

+ 1 - 1
coderedcms/templatetags/coderedcms_tags.py

@@ -152,7 +152,7 @@ def query_update(querydict, key=None, value=None):
         else:
             try:
                 del(get[key])
-            except:
+            except KeyError:
                 pass
     return get
 

+ 2 - 6
coderedcms/tests/settings.py

@@ -12,7 +12,7 @@ https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/
 
 # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
 import os
-from django.utils.translation import gettext_lazy as _
+from django.utils.translation import gettext_lazy as _  # noqa
 
 
 PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -80,7 +80,7 @@ MIDDLEWARE = [
     'django.middleware.security.SecurityMiddleware',
 
     # Error reporting. Uncomment this to recieve emails when a 404 is triggered.
-    #'django.middleware.common.BrokenLinkEmailsMiddleware',
+    # 'django.middleware.common.BrokenLinkEmailsMiddleware',
 
     # CMS functionality
     'wagtail.core.middleware.SiteMiddleware',
@@ -147,10 +147,6 @@ AUTH_PASSWORD_VALIDATORS = [
 
 LANGUAGE_CODE = 'en-us'
 
-LANGUAGES = [
-    ('en-us', _('English')),
-]
-
 TIME_ZONE = 'America/New_York'
 
 USE_I18N = True

+ 0 - 1
coderedcms/tests/urls.py

@@ -1,4 +1,3 @@
-from django.conf import settings
 from django.urls import include, path, re_path
 from django.contrib import admin
 from wagtail.documents import urls as wagtaildocs_urls

+ 11 - 5
coderedcms/urls.py

@@ -18,15 +18,21 @@ urlpatterns = [
     # CodeRed custom URLs
     re_path(r'^sitemap\.xml$', cache_page(sitemap), name='codered_sitemap'),
     re_path(r'^robots\.txt$', cache_page(robots), name='codered_robots'),
-    re_path(r'^{0}(?P<path>.*)$'.format(cr_settings['PROTECTED_MEDIA_URL'].lstrip('/')), serve_protected_file, name="serve_protected_file"),
+    re_path(r'^{0}(?P<path>.*)$'.format(
+        cr_settings['PROTECTED_MEDIA_URL'].lstrip('/')),
+        serve_protected_file,
+        name="serve_protected_file"
+    ),
 
     # Event/Calendar URLs
-    path('ical/generate/single/', event_generate_single_ical_for_event, name='event_generate_single_ical'),
-    path('ical/generate/recurring/', event_generate_recurring_ical_for_event, name='event_generate_recurring_ical'),
-    path('ical/generate/calendar/', event_generate_ical_for_calendar, name='event_generate_ical_for_calendar'),
+    path('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
     re_path(r'', include(wagtailcore_urls)),
-
 ]

+ 11 - 1
coderedcms/utils.py

@@ -5,11 +5,18 @@ from django.utils.html import mark_safe
 
 from coderedcms.settings import cr_settings
 
+
 def get_protected_media_link(request, path, render_link=False):
     if render_link:
-        return mark_safe("<a href='{0}{1}'>{0}{1}</a>".format(request.build_absolute_uri('/')[:-1], path))
+        return mark_safe(
+            "<a href='{0}{1}'>{0}{1}</a>".format(
+                request.build_absolute_uri('/')[:-1],
+                path
+            )
+        )
     return "{0}{1}".format(request.build_absolute_uri('/')[:-1], path)
 
+
 def uri_validator(possible_uri):
     validate = URLValidator()
     try:
@@ -18,6 +25,7 @@ def uri_validator(possible_uri):
     except ValidationError:
         return False
 
+
 def attempt_protected_media_value_conversion(request, value):
     try:
         if value.startswith(cr_settings['PROTECTED_MEDIA_URL']):
@@ -28,6 +36,7 @@ def attempt_protected_media_value_conversion(request, value):
 
     return value
 
+
 def fix_ical_datetime_format(dt_str):
     """
     ICAL generation gives timezones in the format of 2018-06-30T14:00:00-04:00.
@@ -39,6 +48,7 @@ def fix_ical_datetime_format(dt_str):
         return dt_str
     return dt_str
 
+
 def convert_to_amp(value):
     """
     Function that converts non-amp compliant html to valid amp html.

+ 19 - 14
coderedcms/views.py

@@ -7,13 +7,12 @@ from datetime import datetime
 from django.http import Http404, HttpResponse, JsonResponse
 from django.contrib.auth.decorators import login_required
 from django.contrib.contenttypes.models import ContentType
-from django.core.paginator import Paginator
+from django.core.paginator import Paginator, InvalidPage, EmptyPage, PageNotAnInteger
 from django.shortcuts import redirect, render
 from django.utils.translation import ungettext, ugettext_lazy as _
 from icalendar import Calendar
 
 from wagtail.admin import messages
-from wagtail.core.models import Page
 from wagtail.search.backends import db, get_search_backend
 from wagtail.search.models import Query
 
@@ -24,8 +23,6 @@ from coderedcms.importexport import convert_csv_to_json, import_pages, ImportPag
 from coderedcms.settings import cr_settings
 
 
-
-
 def search(request):
     """
     Searches pages across the entire site.
@@ -62,7 +59,7 @@ def search(request):
         if backend.__class__ == db.SearchBackend and db_models:
             for model in db_models:
                 # if search_model is provided, only search on that model
-                if not search_model or search_model == ContentType.objects.get_for_model(model).model:
+                if not search_model or search_model == ContentType.objects.get_for_model(model).model:  # noqa
                     curr_results = model.objects.live().search(search_query)
                     if results:
                         results = list(chain(results, curr_results))
@@ -75,18 +72,23 @@ def search(request):
                 try:
                     model = ContentType.objects.get(model=search_model).model_class()
                     results = model.objects.live().search(search_query)
-                except:
+                except search_model.DoesNotExist:
                     results = None
             else:
-                results = CoderedPage.objects.live().order_by('-last_published_at').search(search_query)
+                results = CoderedPage.objects.live().order_by('-last_published_at').search(search_query)  # noqa
 
         # paginate results
         if results:
-            paginator = Paginator(results, GeneralSettings.for_site(request.site).search_num_results)
+            paginator = Paginator(results, GeneralSettings.for_site(
+                request.site).search_num_results)
             page = request.GET.get('p', 1)
             try:
                 results_paginated = paginator.page(page)
-            except:
+            except PageNotAnInteger:
+                results_paginated = paginator.page(1)
+            except EmptyPage:
+                results_paginated = paginator.page(1)
+            except InvalidPage:
                 results_paginated = paginator.page(1)
 
         # Log the query so Wagtail can suggest promoted results
@@ -159,7 +161,7 @@ def event_generate_recurring_ical_for_event(request):
             try:
                 event = event_page_model.objects.get(pk=event_pk)
                 break
-            except event_page_modal.DoesNotExist:
+            except event_page_model.DoesNotExist:
                 pass
         ical = Calendar()
         for e in event.create_recurring_ical():
@@ -207,15 +209,18 @@ def event_get_calendar_events(request):
 @login_required
 def import_pages_from_csv_file(request):
     """
-    Overwrite of the `import_pages` view from wagtailimportexport.  By default, the `import_pages` view
-    expects a json file to be uploaded.  This view converts the uploaded csv into the json format that
-    the importer expects.
+    Overwrite of the `import_pages` view from wagtailimportexport.  By default, the `import_pages`
+    view expects a json file to be uploaded.  This view converts the uploaded csv into the json
+    format that the importer expects.
     """
 
     if request.method == 'POST':
         form = ImportPagesFromCSVFileForm(request.POST, request.FILES)
         if form.is_valid():
-            import_data = convert_csv_to_json(form.cleaned_data['file'].read().decode('utf-8').splitlines(), form.cleaned_data['page_type'])
+            import_data = convert_csv_to_json(
+                form.cleaned_data['file'].read().decode('utf-8').splitlines(),
+                form.cleaned_data['page_type']
+            )
             parent_page = form.cleaned_data['parent_page']
             try:
                 page_count = import_pages(import_data, parent_page)

+ 5 - 8
coderedcms/wagtail_flexible_forms/models.py

@@ -12,7 +12,6 @@ from django.conf import settings
 from django.contrib import messages
 from django.contrib.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.models import ContentType
-from django.contrib.humanize.templatetags.humanize import naturaltime
 from django.core.files.storage import default_storage
 from django.core.serializers.json import DjangoJSONEncoder
 from django.db.models import (
@@ -20,7 +19,7 @@ from django.db.models import (
     QuerySet,
 )
 from django.db.models.fields.files import FieldFile
-from django.db.models.signals import post_delete, post_save
+from django.db.models.signals import post_delete
 from django.dispatch import receiver
 from django.forms import Form, ImageField, FileField, URLField, EmailField
 from django.http import HttpResponseRedirect
@@ -295,7 +294,7 @@ class SessionFormSubmission(AbstractFormSubmission):
         verbose_name_plural = _('form submissions')
         unique_together = (('page', 'session_key'),
                            ('page', 'user'))
-        abstract=True
+        abstract = True
 
     @property
     def is_complete(self):
@@ -535,7 +534,7 @@ class SubmissionRevision(Model):
 
     class Meta:
         ordering = ('-created_at',)
-        abstract=True
+        abstract = True
 
     @staticmethod
     def get_filters_for(submission):
@@ -598,9 +597,7 @@ class SubmissionRevision(Model):
         if not summary:  # Nothing changed.
             return
         filters.update(
-            type=revision_type,
-            data=json.dumps(data, cls=StreamFormJSONEncoder),
-            summary=summary,
+            type=revision_type, data=json.dumps(data, cls=StreamFormJSONEncoder), summary=summary
         )
         return cls.objects.create(**filters)
 
@@ -838,4 +835,4 @@ class AbstractStreamForm(StreamFormMixin, AbstractForm):
 
 class AbstractEmailStreamForm(StreamFormMixin, AbstractEmailForm):
     class Meta:
-        abstract = True
+        abstract = True

+ 5 - 5
coderedcms/wagtail_flexible_forms/wagtail_hooks.py

@@ -7,7 +7,7 @@ from django.utils.translation import ugettext_lazy as _
 from wagtail.contrib.modeladmin.helpers import (
     PermissionHelper, PagePermissionHelper, PageAdminURLHelper, AdminURLHelper,
     ButtonHelper)
-from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
+from wagtail.contrib.modeladmin.options import ModelAdmin
 from wagtail.contrib.modeladmin.views import IndexView, InstanceSpecificView
 from wagtail.admin import messages
 from wagtail.core import hooks
@@ -168,7 +168,7 @@ class SubmissionButtonHelper(ButtonHelper):
                                       classnames_exclude=classnames_exclude)
 
     def approve_button(self, pk, classnames_add=None,
-                        classnames_exclude=None):
+                       classnames_exclude=None):
         if classnames_add is None:
             classnames_add = []
         if 'button-secondary' in classnames_add:
@@ -288,11 +288,11 @@ class SubmissionAdmin(ModelAdmin):
         form_page = self.get_form_page(request)
         if form_page is None:
             return self.list_display
-        l = []
+        fields = []
         for name, label in form_page.get_data_fields():
-            l.append(name)
+            fields.append(name)
             self.add_data_bridge(name, label)
-        return l
+        return fields
 
     def set_status_view(self, request, instance_pk):
         kwargs = {'model_admin': self, 'instance_pk': instance_pk}

+ 20 - 8
coderedcms/wagtail_hooks.py

@@ -6,23 +6,27 @@ from django.http.response import HttpResponse
 from django.urls import reverse
 from django.utils.html import format_html, mark_safe
 from django.utils.translation import ugettext_lazy as _
-from wagtail.contrib.forms.models import AbstractForm
-from wagtail.contrib.modeladmin.options import modeladmin_register
 from wagtail.core import hooks
 from wagtail.core.models import UserPagePermissionsProxy, get_page_models
 from wagtailcache.cache import clear_cache
 
-from coderedcms import utils
 from coderedcms.wagtail_flexible_forms.wagtail_hooks import FormAdmin, SubmissionAdmin
 
+
 @hooks.register('insert_global_admin_css')
 def global_admin_css():
-    return format_html('<link rel="stylesheet" type="text/css" href="{}">', static('coderedcms/css/codered-admin.css'))
+    return format_html(
+        '<link rel="stylesheet" type="text/css" href="{}">',
+        static('coderedcms/css/codered-admin.css')
+    )
 
 
 @hooks.register('insert_editor_css')
 def editor_css():
-    return format_html('<link rel="stylesheet" type="text/css" href="{}">', static('coderedcms/css/codered-editor.css'))
+    return format_html(
+        '<link rel="stylesheet" type="text/css" href="{}">',
+        static('coderedcms/css/codered-editor.css')
+    )
 
 
 @hooks.register('insert_editor_js')
@@ -58,10 +62,12 @@ def codered_forms(user, editable_forms):
 
     return editable_forms
 
+
 @hooks.register('before_serve_document')
 def serve_document_directly(document, request):
     """
-    This hook prevents documents from being downloaded unless specified by an <a> tag with the download attribute.
+    This hook prevents documents from being downloaded unless
+    specified by an <a> tag with the download attribute.
     """
     content_type, content_encoding = mimetypes.guess_type(document.filename)
     response = HttpResponse(document.file.read(), content_type=content_type)
@@ -94,10 +100,16 @@ class CoderedFormAdmin(FormAdmin):
         actions = []
         if issubclass(type(obj.specific), CoderedFormPage):
             actions.append(
-                '<a href="{0}">{1}</a>'.format(reverse('wagtailforms:list_submissions', args=(obj.pk,)), _('See all Submissions'))
+                '<a href="{0}">{1}</a>'.format(reverse(
+                    'wagtailforms:list_submissions',
+                    args=(obj.pk,)),
+                    _('See all Submissions')
+                )
             )
             actions.append(
-                '<a href="{0}">{1}</a>'.format(reverse('wagtailadmin_pages:edit', args=(obj.pk,)), _('Edit this form page'))
+                '<a href="{0}">{1}</a>'.format(
+                    reverse("wagtailadmin_pages:edit", args=(obj.pk,)), _("Edit this form page")
+                )
             )
         elif issubclass(type(obj.specific), CoderedStreamFormPage):
             actions.append(self.unprocessed_submissions_link(obj))

+ 2 - 2
docs/conf.py

@@ -118,7 +118,7 @@ html_static_path = ['_static']
 # default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
 # 'searchbox.html']``.
 #
-html_sidebars = { '**': ['searchbox.html', 'globaltoc.html', 'relations.html', 'versions.html'] }
+html_sidebars = {'**': ['searchbox.html', 'globaltoc.html', 'relations.html', 'versions.html']}
 
 html_context = {
     'css_files': ['_static/docs.css'],
@@ -199,4 +199,4 @@ epub_title = project
 # epub_uid = ''
 
 # A list of files that should not be packed into the epub file.
-epub_exclude_files = ['search.html']
+epub_exclude_files = ['search.html']

+ 26 - 0
pyproject.toml

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

+ 2 - 2
setup.cfg

@@ -1,3 +1,3 @@
 [flake8]
-exclude = coderedcms/project_template/*,migrations
-max-line-length = 120
+exclude = coderedcms/project_template/*,migrations,schema.py
+max-line-length = 100

+ 17 - 14
setup.py

@@ -2,7 +2,7 @@ import os
 from setuptools import find_packages, setup
 from coderedcms import __version__
 
-with open(os.path.join(os.path.dirname(__file__), 'README.md'), encoding='utf8') as readme:
+with open(os.path.join(os.path.dirname(__file__), "README.md"), encoding="utf8") as readme:
     README = readme.read()
 
 # allow setup.py to be run from any path
@@ -53,20 +53,23 @@ setup(
         'wagtail-import-export>=0.1,<0.2'
     ],
     extras_require={
-        'dev': [
-            'flake8',
-            'libsass',
-            'pytest-cov',
-            'pytest-django',
-            'sphinx',
-            'twine',
-            'wheel'
+        "dev": [
+            # "autopep8",
+            # "black",
+            "flake8",
+            "libsass",
+            "pytest-cov",
+            "pytest-django",
+            "sphinx",
+            "twine",
+            "wheel",
+        ],  # noqa
+        "ci_tests": ["pytest-cov", "pytest-django"],
+        "ci_style": [
+            # "autopep8",
+            # "black",
+            "flake8"
         ],
-        'ci': [
-            'flake8',
-            'pytest-cov',
-            'pytest-django',
-        ]
     },
     entry_points="""
             [console_scripts]