Ver Fonte

Update project template (#604)

Basic template:
* Add `CoderedEventPage` and `CoderedLocationPage` (and associated
models) to the default template. This exposes those models to new
projects without having to write additional code.
* Add a pyproject.toml to prevent Django from trying to blacken
migrations when starting a new project. At some point Django started
doing this and it can potentially cause a huge slowdown.

Sass template (renamed to "pro"):
* Same changes from basic template
* This will be designated as the "professional" template for use in
real-world projects. Therefore, this template provides a full suite of
pre-configured developer tooling.
* Provide `ruff` with configuration.
* Provide `mypy` with configuration.
* Provide `pytest` with configuration.
* Provides custom User, Image, and Document models by default.

Related changes:
* To further avoid lag from the Django template renderer, we now
explicitly define files in project_template which are actually
templates.
* Add pipeline test matrix that checks both basic and pro templates.
Vince Salvino há 1 ano atrás
pai
commit
445e4c4a86
54 ficheiros alterados com 49614 adições e 694 exclusões
  1. 18 10
      azure-pipelines.yml
  2. 10 2
      coderedcms/bin/coderedcms.py
  3. 18 16
      coderedcms/project_template/basic/.gitignore
  4. 3 1
      coderedcms/project_template/basic/manage.py
  5. 9 19
      coderedcms/project_template/basic/project_name/settings/base.py
  6. 1 1
      coderedcms/project_template/basic/project_name/settings/prod.py
  7. 10 7
      coderedcms/project_template/basic/project_name/urls.py
  8. 3 1
      coderedcms/project_template/basic/project_name/wsgi.py
  9. 33 0
      coderedcms/project_template/basic/pyproject.toml
  10. 24273 234
      coderedcms/project_template/basic/website/migrations/0001_initial.py
  11. 0 6
      coderedcms/project_template/basic/website/migrations/0002_initial_data.py
  12. 71 8
      coderedcms/project_template/basic/website/models.py
  13. 7 0
      coderedcms/project_template/pro/.cr.ini
  14. 0 0
      coderedcms/project_template/pro/.editorconfig
  15. 0 0
      coderedcms/project_template/pro/.gitattributes
  16. 18 16
      coderedcms/project_template/pro/.gitignore
  17. 18 0
      coderedcms/project_template/pro/README.md
  18. 0 0
      coderedcms/project_template/pro/custom_media/__init__.py
  19. 8 0
      coderedcms/project_template/pro/custom_media/admin.py
  20. 6 0
      coderedcms/project_template/pro/custom_media/apps.py
  21. 272 0
      coderedcms/project_template/pro/custom_media/migrations/0001_initial.py
  22. 0 0
      coderedcms/project_template/pro/custom_media/migrations/__init__.py
  23. 63 0
      coderedcms/project_template/pro/custom_media/models.py
  24. 0 0
      coderedcms/project_template/pro/custom_user/__init__.py
  25. 54 0
      coderedcms/project_template/pro/custom_user/admin.py
  26. 6 0
      coderedcms/project_template/pro/custom_user/apps.py
  27. 124 0
      coderedcms/project_template/pro/custom_user/migrations/0001_initial.py
  28. 0 0
      coderedcms/project_template/pro/custom_user/migrations/__init__.py
  29. 60 0
      coderedcms/project_template/pro/custom_user/models.py
  30. 3 1
      coderedcms/project_template/pro/manage.py
  31. 0 0
      coderedcms/project_template/pro/project_name/__init__.py
  32. 0 0
      coderedcms/project_template/pro/project_name/settings/__init__.py
  33. 14 14
      coderedcms/project_template/pro/project_name/settings/base.py
  34. 0 0
      coderedcms/project_template/pro/project_name/settings/dev.py
  35. 1 1
      coderedcms/project_template/pro/project_name/settings/prod.py
  36. 10 7
      coderedcms/project_template/pro/project_name/urls.py
  37. 3 1
      coderedcms/project_template/pro/project_name/wsgi.py
  38. 33 0
      coderedcms/project_template/pro/pyproject.toml
  39. 11 0
      coderedcms/project_template/pro/requirements-dev.txt
  40. 0 0
      coderedcms/project_template/pro/requirements.txt
  41. 0 0
      coderedcms/project_template/pro/website/__init__.py
  42. 0 0
      coderedcms/project_template/pro/website/apps.py
  43. 24274 235
      coderedcms/project_template/pro/website/migrations/0001_initial.py
  44. 0 6
      coderedcms/project_template/pro/website/migrations/0002_initial_data.py
  45. 0 0
      coderedcms/project_template/pro/website/migrations/__init__.py
  46. 149 0
      coderedcms/project_template/pro/website/models.py
  47. 0 0
      coderedcms/project_template/pro/website/static/website/js/custom.js
  48. 0 0
      coderedcms/project_template/pro/website/static/website/src/_variables.scss
  49. 0 0
      coderedcms/project_template/pro/website/static/website/src/custom.scss
  50. 0 0
      coderedcms/project_template/pro/website/templates/coderedcms/pages/base.html
  51. 0 2
      coderedcms/project_template/sass/requirements-dev.txt
  52. 0 86
      coderedcms/project_template/sass/website/models.py
  53. 2 2
      coderedcms/tests/test_bin.py
  54. 29 18
      docs/getting_started/install.rst

+ 18 - 10
azure-pipelines.yml

@@ -34,18 +34,27 @@ stages:
         py3.8_wag5.0:
           PYTHON_VERSION: '3.8'
           WAGTAIL_VERSION: '5.0.*'
+          TEMPLATE: 'basic'
         py3.9_wag5.0:
           PYTHON_VERSION: '3.9'
           WAGTAIL_VERSION: '5.0.*'
+          TEMPLATE: 'basic'
         py3.10_wag5.0:
           PYTHON_VERSION: '3.10'
           WAGTAIL_VERSION: '5.0.*'
+          TEMPLATE: 'basic'
         py3.11_wag5.1:
           PYTHON_VERSION: '3.11'
           WAGTAIL_VERSION: '5.1.*'
-        py3.12_wag5.2:
+          TEMPLATE: 'basic'
+        py3.12_wag5.2_basic:
           PYTHON_VERSION: '3.12'
           WAGTAIL_VERSION: '5.2.*'
+          TEMPLATE: 'basic'
+        py3.12_wag5.2_pro:
+          PYTHON_VERSION: '3.12'
+          WAGTAIL_VERSION: '5.2.*'
+          TEMPLATE: 'pro'
 
     steps:
     - task: UsePythonVersion@0
@@ -57,9 +66,16 @@ stages:
     - script: python -m pip install -r requirements-ci.txt wagtail==$(WAGTAIL_VERSION)
       displayName: 'CR-QC: Install coderedcms from local repo'
 
-    - script: coderedcms start testproject
+    - script: coderedcms start testproject --template $(TEMPLATE)
       displayName: 'CR-QC: Create starter project from template'
 
+    - script: |
+        cd testproject/
+        touch requirements-dev.txt
+        python -m pip install -r requirements-dev.txt
+        python manage.py makemigrations --check
+      displayName: 'CR-QC: Check migrations'
+
     - pwsh: ./ci/run-tests.ps1
       displayName: 'CR-QC: Run unit tests'
 
@@ -101,14 +117,6 @@ stages:
     - pwsh: ./ci/spellcheck.ps1
       displayName: 'CR-QC: Spelling'
 
-    - script: coderedcms start testproject
-      displayName: 'CR-QC: Generate a test project'
-
-    - script: |
-        cd testproject/
-        python manage.py makemigrations --check
-      displayName: 'CR-QC: Check migrations'
-
     - script: black --check .
       displayName: 'CR-QC: Black'
 

+ 10 - 2
coderedcms/bin/coderedcms.py

@@ -61,9 +61,17 @@ class CreateProject(TemplateCommand):
         if os.path.isdir(template_path):
             options["template"] = template_path
 
+        # Assume all files are NOT Django templates.
+        options["extensions"] = ["toml"]
         # Treat these files as Django templates to render the boilerplate.
-        options["extensions"] = ["py", "md", "txt"]
-        options["files"] = ["Dockerfile"]
+        options["files"] = [
+            "0002_initial_data.py",
+            "base.py",
+            "manage.py",
+            "README.md",
+            "requirements.txt",
+            "wsgi.py",
+        ]
 
         # Set options
         message = "Creating a Wagtail CRX project called %(project_name)s"

+ 18 - 16
coderedcms/project_template/basic/.gitignore

@@ -1,4 +1,4 @@
-# Created by https://www.gitignore.io, modified for use with CodeRed CMS.
+# Created by https://www.gitignore.io, modified for use with Wagtail CRX.
 
 #######################################
 ### Editors
@@ -197,7 +197,7 @@ tags
 *.pyc
 __pycache__/
 local_settings.py
-db.sqlite3
+*.sqlite3
 
 # If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/
 # in your Git repository. Update and uncomment the following line accordingly.
@@ -315,6 +315,15 @@ dmypy.json
 # Pyre type checker
 .pyre/
 
+# Ruff
+.ruff_cache/
+
+
+
+#######################################
+### Operating Systems
+#######################################
+
 
 ### OSX ###
 
@@ -346,12 +355,6 @@ Temporary Items
 .apdisk
 
 
-
-#######################################
-### Operating Systems
-#######################################
-
-
 ### Windows ###
 
 # Windows thumbnail cache files
@@ -381,19 +384,18 @@ $RECYCLE.BIN/
 
 
 #######################################
-### CodeRed CMS
+### Wagtail CRX
 #######################################
 
 
-#### CodeRed CMS defaults ###
-
 # Cache
 cache/
 
 # File uploads from forms
-protected/
+/protected/
+
+# Media files
+/media/
 
-# if you want to store original uploaded media files in version control,
-# replace "media/" with "media/images/"
-media/
-#media/images/
+# Collected static files
+/static/

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

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

+ 9 - 19
coderedcms/project_template/basic/project_name/settings/base.py

@@ -10,12 +10,11 @@ For the full list of settings and their values, see
 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 pathlib import Path
 
-PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
-BASE_DIR = os.path.dirname(PROJECT_DIR)
+
+# Build paths inside the project like this: BASE_DIR / "subdir".
+BASE_DIR = Path(__file__).resolve().parent.parent.parent
 
 
 # Quick-start development settings - unsuitable for production
@@ -70,8 +69,6 @@ MIDDLEWARE = [
     "django.contrib.auth.middleware.AuthenticationMiddleware",
     "django.middleware.clickjacking.XFrameOptionsMiddleware",
     "django.middleware.security.SecurityMiddleware",
-    #  Error reporting. Uncomment this to receive emails when a 404 is triggered.
-    # 'django.middleware.common.BrokenLinkEmailsMiddleware',
     # CMS functionality
     "wagtail.contrib.redirects.middleware.RedirectMiddleware",
     # Fetch from cache. Must be LAST.
@@ -105,7 +102,7 @@ WSGI_APPLICATION = "{{ project_name }}.wsgi.application"
 DATABASES = {
     "default": {
         "ENGINE": "django.db.backends.sqlite3",
-        "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
+        "NAME": BASE_DIR / "db.sqlite3",
     }
 }
 
@@ -128,17 +125,15 @@ AUTH_PASSWORD_VALIDATORS = [
     },
 ]
 
+
 # Internationalization
 # https://docs.djangoproject.com/en/{{ docs_version }}/topics/i18n/
 
-# To add or change language of the project, modify the list below.
 LANGUAGE_CODE = "en-us"
 
-LANGUAGES = [("en-us", _("English"))]
-
 TIME_ZONE = "America/New_York"
 
-USE_I18N = True
+USE_I18N = False
 
 USE_TZ = True
 
@@ -151,10 +146,10 @@ STATICFILES_FINDERS = [
     "django.contrib.staticfiles.finders.AppDirectoriesFinder",
 ]
 
-STATIC_ROOT = os.path.join(BASE_DIR, "static")
+STATIC_ROOT = BASE_DIR / "static"
 STATIC_URL = "/static/"
 
-MEDIA_ROOT = os.path.join(BASE_DIR, "media")
+MEDIA_ROOT = BASE_DIR / "media"
 MEDIA_URL = "/media/"
 
 
@@ -170,11 +165,6 @@ WAGTAIL_SITE_NAME = "{{ sitename }}"
 
 WAGTAIL_ENABLE_UPDATE_CHECK = False
 
-WAGTAILSEARCH_BACKENDS = {
-    "default": {
-        "BACKEND": "wagtail.search.backends.database",
-    }
-}
 
 # Base URL to use when referring to full URLs within the Wagtail admin backend -
 # e.g. in notification emails. Don't include '/admin' or a trailing slash

+ 1 - 1
coderedcms/project_template/basic/project_name/settings/prod.py

@@ -32,7 +32,7 @@ SERVER_EMAIL = DEFAULT_FROM_EMAIL
 CACHES = {
     "default": {
         "BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
-        "LOCATION": os.path.join(BASE_DIR, "cache"),  # noqa
+        "LOCATION": BASE_DIR / "cache",  # noqa
         "KEY_PREFIX": "coderedcms",
         "TIMEOUT": 14400,  # in seconds
     }

+ 10 - 7
coderedcms/project_template/basic/project_name/urls.py

@@ -1,10 +1,12 @@
-from django.conf import settings
-from django.urls import include, path
-from django.contrib import admin
-from wagtail.documents import urls as wagtaildocs_urls
 from coderedcms import admin_urls as crx_admin_urls
 from coderedcms import search_urls as crx_search_urls
 from coderedcms import urls as crx_urls
+from django.conf import settings
+from django.contrib import admin
+from django.urls import include
+from django.urls import path
+from wagtail.documents import urls as wagtaildocs_urls
+
 
 urlpatterns = [
     # Admin
@@ -24,10 +26,11 @@ urlpatterns = [
 ]
 
 
+# fmt: off
 if settings.DEBUG:
     from django.conf.urls.static import static
-    from django.contrib.staticfiles.urls import staticfiles_urlpatterns
 
     # Serve static and media files from development server
-    urlpatterns += staticfiles_urlpatterns()
-    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
+    urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)  # type: ignore
+    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)  # type: ignore
+# fmt: on

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

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

+ 33 - 0
coderedcms/project_template/basic/pyproject.toml

@@ -0,0 +1,33 @@
+[tool.black]
+line-length = 80
+extend-exclude = ["migrations"]
+
+[tool.django-stubs]
+django_settings_module = "{{ project_name }}.settings.dev"
+
+[tool.mypy]
+ignore_missing_imports = true
+plugins = ["mypy_django_plugin.main"]
+exclude = [
+    '^\..*',
+    'migrations',
+    'node_modules',
+    'venv',
+]
+
+[tool.pytest.ini_options]
+DJANGO_SETTINGS_MODULE = "{{ project_name }}.settings.dev"
+addopts = "--cov --cov-report html"
+python_files = "tests.py test_*.py"
+
+[tool.ruff]
+extend-exclude = ["migrations"]
+line-length = 80
+
+[tool.ruff.lint]
+extend-select = ["I"]
+
+[tool.ruff.lint.isort]
+case-sensitive = false
+force-single-line = true
+lines-after-imports = 2

Diff do ficheiro suprimidas por serem muito extensas
+ 24273 - 234
coderedcms/project_template/basic/website/migrations/0001_initial.py


+ 0 - 6
coderedcms/project_template/basic/website/migrations/0002_initial_data.py

@@ -1,10 +1,4 @@
-
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
-from django import VERSION as DJANGO_VERSION
 from django.db import migrations
-
 from wagtail.models import Locale
 
 

+ 71 - 8
coderedcms/project_template/basic/website/models.py

@@ -1,15 +1,18 @@
 """
 Create or customize your page models here.
 """
-from modelcluster.fields import ParentalKey
 from coderedcms.forms import CoderedFormField
-from coderedcms.models import (
-    CoderedArticlePage,
-    CoderedArticleIndexPage,
-    CoderedEmail,
-    CoderedFormPage,
-    CoderedWebPage,
-)
+from coderedcms.models import CoderedArticleIndexPage
+from coderedcms.models import CoderedArticlePage
+from coderedcms.models import CoderedEmail
+from coderedcms.models import CoderedEventIndexPage
+from coderedcms.models import CoderedEventOccurrence
+from coderedcms.models import CoderedEventPage
+from coderedcms.models import CoderedFormPage
+from coderedcms.models import CoderedLocationIndexPage
+from coderedcms.models import CoderedLocationPage
+from coderedcms.models import CoderedWebPage
+from modelcluster.fields import ParentalKey
 
 
 class ArticlePage(CoderedArticlePage):
@@ -45,6 +48,34 @@ class ArticleIndexPage(CoderedArticleIndexPage):
     template = "coderedcms/pages/article_index_page.html"
 
 
+class EventPage(CoderedEventPage):
+    class Meta:
+        verbose_name = "Event Page"
+
+    parent_page_types = ["website.EventIndexPage"]
+    template = "coderedcms/pages/event_page.html"
+
+
+class EventIndexPage(CoderedEventIndexPage):
+    """
+    Shows a list of event sub-pages.
+    """
+
+    class Meta:
+        verbose_name = "Events Landing Page"
+
+    index_query_pagemodel = "website.EventPage"
+
+    # Only allow EventPages beneath this page.
+    subpage_types = ["website.EventPage"]
+
+    template = "coderedcms/pages/event_index_page.html"
+
+
+class EventOccurrence(CoderedEventOccurrence):
+    event = ParentalKey(EventPage, related_name="occurrences")
+
+
 class FormPage(CoderedFormPage):
     """
     A page with an html <form>.
@@ -75,6 +106,38 @@ class FormConfirmEmail(CoderedEmail):
     page = ParentalKey("FormPage", related_name="confirmation_emails")
 
 
+class LocationPage(CoderedLocationPage):
+    """
+    A page that holds a location.  This could be a store, a restaurant, etc.
+    """
+
+    class Meta:
+        verbose_name = "Location Page"
+
+    template = "coderedcms/pages/location_page.html"
+
+    # Only allow LocationIndexPages above this page.
+    parent_page_types = ["website.LocationIndexPage"]
+
+
+class LocationIndexPage(CoderedLocationIndexPage):
+    """
+    A page that holds a list of locations and displays them with a Google Map.
+    This does require a Google Maps API Key in Settings > CRX Settings
+    """
+
+    class Meta:
+        verbose_name = "Location Landing Page"
+
+    # Override to specify custom index ordering choice/default.
+    index_query_pagemodel = "website.LocationPage"
+
+    # Only allow LocationPages beneath this page.
+    subpage_types = ["website.LocationPage"]
+
+    template = "coderedcms/pages/location_index_page.html"
+
+
 class WebPage(CoderedWebPage):
     """
     General use page with featureful streamfield and SEO attributes.

+ 7 - 0
coderedcms/project_template/pro/.cr.ini

@@ -0,0 +1,7 @@
+# This is a deployment file for CodeRed Cloud hosting.
+# If you are not using CodeRed Cloud, you can delete this file.
+#
+# https://www.codered.cloud/cli/
+#
+[cr]
+deploy_include = website/static/website/css/custom.css

+ 0 - 0
coderedcms/project_template/sass/.editorconfig → coderedcms/project_template/pro/.editorconfig


+ 0 - 0
coderedcms/project_template/sass/.gitattributes → coderedcms/project_template/pro/.gitattributes


+ 18 - 16
coderedcms/project_template/sass/.gitignore → coderedcms/project_template/pro/.gitignore

@@ -1,4 +1,4 @@
-# Created by https://www.gitignore.io, modified for use with CodeRed CMS.
+# Created by https://www.gitignore.io, modified for use with Wagtail CRX.
 
 #######################################
 ### Editors
@@ -197,7 +197,7 @@ tags
 *.pyc
 __pycache__/
 local_settings.py
-db.sqlite3
+*.sqlite3
 
 # If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/
 # in your Git repository. Update and uncomment the following line accordingly.
@@ -315,6 +315,15 @@ dmypy.json
 # Pyre type checker
 .pyre/
 
+# Ruff
+.ruff_cache/
+
+
+
+#######################################
+### Operating Systems
+#######################################
+
 
 ### OSX ###
 
@@ -346,12 +355,6 @@ Temporary Items
 .apdisk
 
 
-
-#######################################
-### Operating Systems
-#######################################
-
-
 ### Windows ###
 
 # Windows thumbnail cache files
@@ -381,19 +384,18 @@ $RECYCLE.BIN/
 
 
 #######################################
-### CodeRed CMS
+### Wagtail CRX
 #######################################
 
 
-#### CodeRed CMS defaults ###
-
 # Cache
 cache/
 
 # File uploads from forms
-protected/
+/protected/
+
+# Media files
+/media/
 
-# if you want to store original uploaded media files in version control,
-# replace "media/" with "media/images/"
-media/
-#media/images/
+# Collected static files
+/static/

+ 18 - 0
coderedcms/project_template/sass/README.md → coderedcms/project_template/pro/README.md

@@ -32,6 +32,24 @@ Open this directory in a command prompt, then:
    to log in and get to work!
 
 
+## Linting / pre-deployment
+
+To check for errors, run the following commands:
+
+```
+ruff check --fix .
+ruff format .
+mypy .
+pytest .
+```
+
+Before deploying, be sure to build the sass:
+
+```
+python manage.py sass -t compressed website/static/website/src/custom.scss website/static/website/css/
+```
+
+
 ## Documentation links
 
 * To customize the content, design, and features of the site see

+ 0 - 0
coderedcms/project_template/sass/project_name/__init__.py → coderedcms/project_template/pro/custom_media/__init__.py


+ 8 - 0
coderedcms/project_template/pro/custom_media/admin.py

@@ -0,0 +1,8 @@
+from django.contrib import admin
+
+from .models import CustomDocument
+from .models import CustomImage
+
+
+admin.site.register(CustomDocument)
+admin.site.register(CustomImage)

+ 6 - 0
coderedcms/project_template/pro/custom_media/apps.py

@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class CustomMediaConfig(AppConfig):
+    default_auto_field = "django.db.models.BigAutoField"
+    name = "custom_media"

+ 272 - 0
coderedcms/project_template/pro/custom_media/migrations/0001_initial.py

@@ -0,0 +1,272 @@
+# Generated by Django 4.2.7 on 2023-11-03 22:24
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import taggit.managers
+import wagtail.images.models
+import wagtail.models.collections
+import wagtail.search.index
+
+
+class Migration(migrations.Migration):
+    initial = True
+
+    dependencies = [
+        ("taggit", "0005_auto_20220424_2025"),
+        ("wagtailcore", "0083_workflowcontenttype"),
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="CustomImage",
+            fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "title",
+                    models.CharField(max_length=255, verbose_name="title"),
+                ),
+                (
+                    "file",
+                    wagtail.images.models.WagtailImageField(
+                        height_field="height",
+                        upload_to=wagtail.images.models.get_upload_to,
+                        verbose_name="file",
+                        width_field="width",
+                    ),
+                ),
+                (
+                    "width",
+                    models.IntegerField(editable=False, verbose_name="width"),
+                ),
+                (
+                    "height",
+                    models.IntegerField(editable=False, verbose_name="height"),
+                ),
+                (
+                    "created_at",
+                    models.DateTimeField(
+                        auto_now_add=True,
+                        db_index=True,
+                        verbose_name="created at",
+                    ),
+                ),
+                (
+                    "focal_point_x",
+                    models.PositiveIntegerField(blank=True, null=True),
+                ),
+                (
+                    "focal_point_y",
+                    models.PositiveIntegerField(blank=True, null=True),
+                ),
+                (
+                    "focal_point_width",
+                    models.PositiveIntegerField(blank=True, null=True),
+                ),
+                (
+                    "focal_point_height",
+                    models.PositiveIntegerField(blank=True, null=True),
+                ),
+                (
+                    "file_size",
+                    models.PositiveIntegerField(editable=False, null=True),
+                ),
+                (
+                    "file_hash",
+                    models.CharField(
+                        blank=True, db_index=True, editable=False, max_length=40
+                    ),
+                ),
+                (
+                    "alt_text",
+                    models.CharField(
+                        blank=True,
+                        help_text="A description of this image used by search engines and screen readers.",
+                        max_length=255,
+                        verbose_name="Alt Text",
+                    ),
+                ),
+                (
+                    "credit",
+                    models.CharField(
+                        blank=True,
+                        help_text="Credit or attribute the source of the image. Properly attributing images taken from online sources can reduce your risk of copyright infringement.",
+                        max_length=255,
+                        verbose_name="Credit",
+                    ),
+                ),
+                (
+                    "collection",
+                    models.ForeignKey(
+                        default=wagtail.models.collections.get_root_collection_id,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="+",
+                        to="wagtailcore.collection",
+                        verbose_name="collection",
+                    ),
+                ),
+                (
+                    "tags",
+                    taggit.managers.TaggableManager(
+                        blank=True,
+                        help_text=None,
+                        through="taggit.TaggedItem",
+                        to="taggit.Tag",
+                        verbose_name="tags",
+                    ),
+                ),
+                (
+                    "uploaded_by_user",
+                    models.ForeignKey(
+                        blank=True,
+                        editable=False,
+                        null=True,
+                        on_delete=django.db.models.deletion.SET_NULL,
+                        to=settings.AUTH_USER_MODEL,
+                        verbose_name="uploaded by user",
+                    ),
+                ),
+            ],
+            options={
+                "abstract": False,
+            },
+            bases=(
+                wagtail.images.models.ImageFileMixin,
+                wagtail.search.index.Indexed,
+                models.Model,
+            ),
+        ),
+        migrations.CreateModel(
+            name="CustomDocument",
+            fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "title",
+                    models.CharField(max_length=255, verbose_name="title"),
+                ),
+                (
+                    "file",
+                    models.FileField(
+                        upload_to="documents", verbose_name="file"
+                    ),
+                ),
+                (
+                    "created_at",
+                    models.DateTimeField(
+                        auto_now_add=True, verbose_name="created at"
+                    ),
+                ),
+                (
+                    "file_size",
+                    models.PositiveIntegerField(editable=False, null=True),
+                ),
+                (
+                    "file_hash",
+                    models.CharField(blank=True, editable=False, max_length=40),
+                ),
+                (
+                    "collection",
+                    models.ForeignKey(
+                        default=wagtail.models.collections.get_root_collection_id,
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="+",
+                        to="wagtailcore.collection",
+                        verbose_name="collection",
+                    ),
+                ),
+                (
+                    "tags",
+                    taggit.managers.TaggableManager(
+                        blank=True,
+                        help_text=None,
+                        through="taggit.TaggedItem",
+                        to="taggit.Tag",
+                        verbose_name="tags",
+                    ),
+                ),
+                (
+                    "uploaded_by_user",
+                    models.ForeignKey(
+                        blank=True,
+                        editable=False,
+                        null=True,
+                        on_delete=django.db.models.deletion.SET_NULL,
+                        to=settings.AUTH_USER_MODEL,
+                        verbose_name="uploaded by user",
+                    ),
+                ),
+            ],
+            options={
+                "verbose_name": "document",
+                "verbose_name_plural": "documents",
+                "abstract": False,
+            },
+            bases=(wagtail.search.index.Indexed, models.Model),
+        ),
+        migrations.CreateModel(
+            name="CustomRendition",
+            fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "filter_spec",
+                    models.CharField(db_index=True, max_length=255),
+                ),
+                (
+                    "file",
+                    wagtail.images.models.WagtailImageField(
+                        height_field="height",
+                        storage=wagtail.images.models.get_rendition_storage,
+                        upload_to=wagtail.images.models.get_rendition_upload_to,
+                        width_field="width",
+                    ),
+                ),
+                ("width", models.IntegerField(editable=False)),
+                ("height", models.IntegerField(editable=False)),
+                (
+                    "focal_point_key",
+                    models.CharField(
+                        blank=True, default="", editable=False, max_length=16
+                    ),
+                ),
+                (
+                    "image",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        related_name="renditions",
+                        to="custom_media.customimage",
+                    ),
+                ),
+            ],
+            options={
+                "unique_together": {
+                    ("image", "filter_spec", "focal_point_key")
+                },
+            },
+            bases=(wagtail.images.models.ImageFileMixin, models.Model),
+        ),
+    ]

+ 0 - 0
coderedcms/project_template/sass/project_name/settings/__init__.py → coderedcms/project_template/pro/custom_media/migrations/__init__.py


+ 63 - 0
coderedcms/project_template/pro/custom_media/models.py

@@ -0,0 +1,63 @@
+"""
+Custom overrides of Wagtail Document and Image models. All other
+models related to website content should most likely go in
+``website.models`` instead.
+"""
+from django.db import models
+from wagtail.documents.models import AbstractDocument
+from wagtail.documents.models import Document
+from wagtail.images.models import AbstractImage
+from wagtail.images.models import AbstractRendition
+from wagtail.images.models import Image
+
+
+class CustomDocument(AbstractDocument):
+    """
+    A custom Wagtail Document model. Right now it is the same as
+    the default, but can be easily extended by adding more fields here.
+    """
+
+    admin_form_fields = Document.admin_form_fields
+
+
+class CustomImage(AbstractImage):
+    """
+    A custom Wagtail Image model with fields for alt text and
+    credit/attribution.
+    """
+
+    alt_text = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name="Alt Text",
+        help_text=(
+            "A description of this image used by search engines and screen readers."
+        ),
+    )
+    credit = models.CharField(
+        max_length=255,
+        blank=True,
+        verbose_name="Credit",
+        help_text=(
+            "Credit or attribute the source of the image. "
+            "Properly attributing images taken from online sources can "
+            "reduce your risk of copyright infringement."
+        ),
+    )
+    admin_form_fields = Image.admin_form_fields + (
+        "alt_text",
+        "credit",
+    )
+
+
+class CustomRendition(AbstractRendition):
+    """
+    Image rendition for our CustomImage model.
+    """
+
+    image = models.ForeignKey(
+        CustomImage, on_delete=models.CASCADE, related_name="renditions"
+    )
+
+    class Meta:
+        unique_together = (("image", "filter_spec", "focal_point_key"),)

+ 0 - 0
coderedcms/project_template/sass/website/__init__.py → coderedcms/project_template/pro/custom_user/__init__.py


+ 54 - 0
coderedcms/project_template/pro/custom_user/admin.py

@@ -0,0 +1,54 @@
+from django.contrib import admin
+from django.contrib.auth.admin import UserAdmin
+from django.utils.translation import gettext_lazy as _
+
+from .models import User
+
+
+class CustomUserAdmin(UserAdmin):
+    """
+    Override Django's default UserAdmin to use the email address
+    as the identifier, instead of username.
+    """
+
+    list_display = ["email", "first_name", "last_name", "is_staff"]
+    search_fields = ["email", "first_name", "last_name"]
+    ordering = ["email"]
+    filter_horizontal = ["groups", "user_permissions"]
+    fieldsets = (
+        (None, {"fields": ("email", "password")}),
+        (
+            _("Personal info"),
+            {
+                "fields": (
+                    "first_name",
+                    "last_name",
+                )
+            },
+        ),
+        (
+            _("Permissions"),
+            {
+                "fields": (
+                    "is_active",
+                    "is_staff",
+                    "is_superuser",
+                    "groups",
+                    "user_permissions",
+                ),
+            },
+        ),
+        (_("Important dates"), {"fields": ("last_login", "date_joined")}),
+    )
+    add_fieldsets = (
+        (
+            None,
+            {
+                "classes": ("wide",),
+                "fields": ("email", "password1", "password2"),
+            },
+        ),
+    )
+
+
+admin.site.register(User, CustomUserAdmin)

+ 6 - 0
coderedcms/project_template/pro/custom_user/apps.py

@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class CustomUserConfig(AppConfig):
+    default_auto_field = "django.db.models.BigAutoField"
+    name = "custom_user"

+ 124 - 0
coderedcms/project_template/pro/custom_user/migrations/0001_initial.py

@@ -0,0 +1,124 @@
+# Generated by Django 4.2.7 on 2023-11-03 22:24
+
+import custom_user.models
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+    initial = True
+
+    dependencies = [
+        ("auth", "0012_alter_user_first_name_max_length"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="User",
+            fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "password",
+                    models.CharField(max_length=128, verbose_name="password"),
+                ),
+                (
+                    "last_login",
+                    models.DateTimeField(
+                        blank=True, null=True, verbose_name="last login"
+                    ),
+                ),
+                (
+                    "is_superuser",
+                    models.BooleanField(
+                        default=False,
+                        help_text="Designates that this user has all permissions without explicitly assigning them.",
+                        verbose_name="superuser status",
+                    ),
+                ),
+                (
+                    "first_name",
+                    models.CharField(
+                        blank=True, max_length=150, verbose_name="first name"
+                    ),
+                ),
+                (
+                    "last_name",
+                    models.CharField(
+                        blank=True, max_length=150, verbose_name="last name"
+                    ),
+                ),
+                (
+                    "is_staff",
+                    models.BooleanField(
+                        default=False,
+                        help_text="Designates whether the user can log into this admin site.",
+                        verbose_name="staff status",
+                    ),
+                ),
+                (
+                    "is_active",
+                    models.BooleanField(
+                        default=True,
+                        help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
+                        verbose_name="active",
+                    ),
+                ),
+                (
+                    "date_joined",
+                    models.DateTimeField(
+                        default=django.utils.timezone.now,
+                        verbose_name="date joined",
+                    ),
+                ),
+                (
+                    "email",
+                    models.EmailField(
+                        error_messages={
+                            "unique": "A user with that username already exists."
+                        },
+                        max_length=254,
+                        unique=True,
+                        verbose_name="email address",
+                    ),
+                ),
+                (
+                    "groups",
+                    models.ManyToManyField(
+                        blank=True,
+                        help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
+                        related_name="user_set",
+                        related_query_name="user",
+                        to="auth.group",
+                        verbose_name="groups",
+                    ),
+                ),
+                (
+                    "user_permissions",
+                    models.ManyToManyField(
+                        blank=True,
+                        help_text="Specific permissions for this user.",
+                        related_name="user_set",
+                        related_query_name="user",
+                        to="auth.permission",
+                        verbose_name="user permissions",
+                    ),
+                ),
+            ],
+            options={
+                "verbose_name": "user",
+                "verbose_name_plural": "users",
+                "abstract": False,
+            },
+            managers=[
+                ("objects", custom_user.models.UserManager()),
+            ],
+        ),
+    ]

+ 0 - 0
coderedcms/project_template/sass/website/migrations/__init__.py → coderedcms/project_template/pro/custom_user/migrations/__init__.py


+ 60 - 0
coderedcms/project_template/pro/custom_user/models.py

@@ -0,0 +1,60 @@
+from django.contrib.auth.base_user import BaseUserManager
+from django.contrib.auth.models import AbstractUser
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+
+
+class UserManager(BaseUserManager):
+    """
+    Model manager for our custom User object, which uses email
+    addresses instead of usernames.
+    """
+
+    use_in_migrations = True
+
+    def _create_user(self, email, password, **extra_fields):
+        if not email:
+            raise ValueError("Email address must be provided.")
+        email = self.normalize_email(email)
+        user = self.model(email=email, **extra_fields)
+        user.set_password(password)
+        user.save(using=self._db)
+        return user
+
+    def create_user(self, email, password=None, **extra_fields):
+        extra_fields.setdefault("is_staff", False)
+        extra_fields.setdefault("is_superuser", False)
+        return self._create_user(email, password, **extra_fields)
+
+    def create_superuser(self, email, password=None, **extra_fields):
+        extra_fields.setdefault("is_staff", True)
+        extra_fields.setdefault("is_superuser", True)
+
+        if extra_fields.get("is_staff") is not True:
+            raise ValueError("Superuser must have is_staff=True.")
+        if extra_fields.get("is_superuser") is not True:
+            raise ValueError("Superuser must have is_superuser=True.")
+
+        return self._create_user(email, password, **extra_fields)
+
+
+class User(AbstractUser):  # type: ignore
+    """
+    A custom user model, which uses email instead of username as
+    the identifier.
+    """
+
+    username = None  # type: ignore
+
+    email = models.EmailField(
+        _("email address"),
+        unique=True,
+        error_messages={
+            "unique": _("A user with that username already exists."),
+        },
+    )
+
+    objects = UserManager()  # type: ignore
+
+    USERNAME_FIELD = "email"
+    REQUIRED_FIELDS = []

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

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

+ 0 - 0
coderedcms/project_template/sass/website/static/website/js/custom.js → coderedcms/project_template/pro/project_name/__init__.py


+ 0 - 0
coderedcms/project_template/pro/project_name/settings/__init__.py


+ 14 - 14
coderedcms/project_template/sass/project_name/settings/base.py → coderedcms/project_template/pro/project_name/settings/base.py

@@ -10,11 +10,11 @@ For the full list of settings and their values, see
 https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/
 """
 
-# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
-import os
+from pathlib import Path
 
-PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
-BASE_DIR = os.path.dirname(PROJECT_DIR)
+
+# Build paths inside the project like this: BASE_DIR / "subdir".
+BASE_DIR = Path(__file__).resolve().parent.parent.parent
 
 
 # Quick-start development settings - unsuitable for production
@@ -26,6 +26,8 @@ BASE_DIR = os.path.dirname(PROJECT_DIR)
 INSTALLED_APPS = [
     # This project
     "website",
+    "custom_media",
+    "custom_user",
     # Wagtail CRX (CodeRed Extensions)
     "coderedcms",
     "django_bootstrap5",
@@ -69,8 +71,6 @@ MIDDLEWARE = [
     "django.contrib.auth.middleware.AuthenticationMiddleware",
     "django.middleware.clickjacking.XFrameOptionsMiddleware",
     "django.middleware.security.SecurityMiddleware",
-    # Error reporting. Uncomment this to receive emails when a 404 is triggered.
-    # 'django.middleware.common.BrokenLinkEmailsMiddleware',
     # CMS functionality
     "wagtail.contrib.redirects.middleware.RedirectMiddleware",
     # Fetch from cache. Must be LAST.
@@ -104,7 +104,7 @@ WSGI_APPLICATION = "{{ project_name }}.wsgi.application"
 DATABASES = {
     "default": {
         "ENGINE": "django.db.backends.sqlite3",
-        "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
+        "NAME": BASE_DIR / "db.sqlite3",
     }
 }
 
@@ -127,6 +127,8 @@ AUTH_PASSWORD_VALIDATORS = [
     },
 ]
 
+AUTH_USER_MODEL = "custom_user.User"
+
 
 # Internationalization
 # https://docs.djangoproject.com/en/{{ docs_version }}/topics/i18n/
@@ -148,10 +150,10 @@ STATICFILES_FINDERS = [
     "django.contrib.staticfiles.finders.AppDirectoriesFinder",
 ]
 
-STATIC_ROOT = os.path.join(BASE_DIR, "static")
+STATIC_ROOT = BASE_DIR / "static"
 STATIC_URL = "/static/"
 
-MEDIA_ROOT = os.path.join(BASE_DIR, "media")
+MEDIA_ROOT = BASE_DIR / "media"
 MEDIA_URL = "/media/"
 
 
@@ -167,11 +169,9 @@ WAGTAIL_SITE_NAME = "{{ sitename }}"
 
 WAGTAIL_ENABLE_UPDATE_CHECK = False
 
-WAGTAILSEARCH_BACKENDS = {
-    "default": {
-        "BACKEND": "wagtail.search.backends.database",
-    }
-}
+WAGTAILIMAGES_IMAGE_MODEL = "custom_media.CustomImage"
+
+WAGTAILDOCS_DOCUMENT_MODEL = "custom_media.CustomDocument"
 
 # Base URL to use when referring to full URLs within the Wagtail admin backend -
 # e.g. in notification emails. Don't include '/admin' or a trailing slash

+ 0 - 0
coderedcms/project_template/sass/project_name/settings/dev.py → coderedcms/project_template/pro/project_name/settings/dev.py


+ 1 - 1
coderedcms/project_template/sass/project_name/settings/prod.py → coderedcms/project_template/pro/project_name/settings/prod.py

@@ -32,7 +32,7 @@ SERVER_EMAIL = DEFAULT_FROM_EMAIL
 CACHES = {
     "default": {
         "BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
-        "LOCATION": os.path.join(BASE_DIR, "cache"),  # noqa
+        "LOCATION": BASE_DIR / "cache",  # noqa
         "KEY_PREFIX": "coderedcms",
         "TIMEOUT": 14400,  # in seconds
     }

+ 10 - 7
coderedcms/project_template/sass/project_name/urls.py → coderedcms/project_template/pro/project_name/urls.py

@@ -1,10 +1,12 @@
-from django.conf import settings
-from django.urls import include, path
-from django.contrib import admin
-from wagtail.documents import urls as wagtaildocs_urls
 from coderedcms import admin_urls as crx_admin_urls
 from coderedcms import search_urls as crx_search_urls
 from coderedcms import urls as crx_urls
+from django.conf import settings
+from django.contrib import admin
+from django.urls import include
+from django.urls import path
+from wagtail.documents import urls as wagtaildocs_urls
+
 
 urlpatterns = [
     # Admin
@@ -24,10 +26,11 @@ urlpatterns = [
 ]
 
 
+# fmt: off
 if settings.DEBUG:
     from django.conf.urls.static import static
-    from django.contrib.staticfiles.urls import staticfiles_urlpatterns
 
     # Serve static and media files from development server
-    urlpatterns += staticfiles_urlpatterns()
-    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
+    urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)  # type: ignore
+    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)  # type: ignore
+# fmt: on

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

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

+ 33 - 0
coderedcms/project_template/pro/pyproject.toml

@@ -0,0 +1,33 @@
+[tool.black]
+line-length = 80
+extend-exclude = ["migrations"]
+
+[tool.django-stubs]
+django_settings_module = "{{ project_name }}.settings.dev"
+
+[tool.mypy]
+ignore_missing_imports = true
+plugins = ["mypy_django_plugin.main"]
+exclude = [
+    '^\..*',
+    'migrations',
+    'node_modules',
+    'venv',
+]
+
+[tool.pytest.ini_options]
+DJANGO_SETTINGS_MODULE = "{{ project_name }}.settings.dev"
+addopts = "--cov --cov-report html"
+python_files = "tests.py test_*.py"
+
+[tool.ruff]
+extend-exclude = ["migrations"]
+line-length = 80
+
+[tool.ruff.lint]
+extend-select = ["I"]
+
+[tool.ruff.lint.isort]
+case-sensitive = false
+force-single-line = true
+lines-after-imports = 2

+ 11 - 0
coderedcms/project_template/pro/requirements-dev.txt

@@ -0,0 +1,11 @@
+# Install normal requirements
+-r requirements.txt
+
+# Tooling for local development
+django-sass
+django-stubs
+mypy
+pytest
+pytest-cov
+pytest-django
+ruff

+ 0 - 0
coderedcms/project_template/sass/requirements.txt → coderedcms/project_template/pro/requirements.txt


+ 0 - 0
coderedcms/project_template/pro/website/__init__.py


+ 0 - 0
coderedcms/project_template/sass/website/apps.py → coderedcms/project_template/pro/website/apps.py


Diff do ficheiro suprimidas por serem muito extensas
+ 24274 - 235
coderedcms/project_template/pro/website/migrations/0001_initial.py


+ 0 - 6
coderedcms/project_template/sass/website/migrations/0002_initial_data.py → coderedcms/project_template/pro/website/migrations/0002_initial_data.py

@@ -1,10 +1,4 @@
-
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
-from django import VERSION as DJANGO_VERSION
 from django.db import migrations
-
 from wagtail.models import Locale
 
 

+ 0 - 0
coderedcms/project_template/pro/website/migrations/__init__.py


+ 149 - 0
coderedcms/project_template/pro/website/models.py

@@ -0,0 +1,149 @@
+"""
+Create or customize your page models here.
+"""
+from coderedcms.forms import CoderedFormField
+from coderedcms.models import CoderedArticleIndexPage
+from coderedcms.models import CoderedArticlePage
+from coderedcms.models import CoderedEmail
+from coderedcms.models import CoderedEventIndexPage
+from coderedcms.models import CoderedEventOccurrence
+from coderedcms.models import CoderedEventPage
+from coderedcms.models import CoderedFormPage
+from coderedcms.models import CoderedLocationIndexPage
+from coderedcms.models import CoderedLocationPage
+from coderedcms.models import CoderedWebPage
+from modelcluster.fields import ParentalKey
+
+
+class ArticlePage(CoderedArticlePage):
+    """
+    Article, suitable for news or blog content.
+    """
+
+    class Meta:
+        verbose_name = "Article"
+        ordering = ["-first_published_at"]
+
+    # Only allow this page to be created beneath an ArticleIndexPage.
+    parent_page_types = ["website.ArticleIndexPage"]
+
+    template = "coderedcms/pages/article_page.html"
+    search_template = "coderedcms/pages/article_page.search.html"
+
+
+class ArticleIndexPage(CoderedArticleIndexPage):
+    """
+    Shows a list of article sub-pages.
+    """
+
+    class Meta:
+        verbose_name = "Article Landing Page"
+
+    # Override to specify custom index ordering choice/default.
+    index_query_pagemodel = "website.ArticlePage"
+
+    # Only allow ArticlePages beneath this page.
+    subpage_types = ["website.ArticlePage"]
+
+    template = "coderedcms/pages/article_index_page.html"
+
+
+class EventPage(CoderedEventPage):
+    class Meta:
+        verbose_name = "Event Page"
+
+    parent_page_types = ["website.EventIndexPage"]
+    template = "coderedcms/pages/event_page.html"
+
+
+class EventIndexPage(CoderedEventIndexPage):
+    """
+    Shows a list of event sub-pages.
+    """
+
+    class Meta:
+        verbose_name = "Events Landing Page"
+
+    index_query_pagemodel = "website.EventPage"
+
+    # Only allow EventPages beneath this page.
+    subpage_types = ["website.EventPage"]
+
+    template = "coderedcms/pages/event_index_page.html"
+
+
+class EventOccurrence(CoderedEventOccurrence):
+    event = ParentalKey(EventPage, related_name="occurrences")
+
+
+class FormPage(CoderedFormPage):
+    """
+    A page with an html <form>.
+    """
+
+    class Meta:
+        verbose_name = "Form"
+
+    template = "coderedcms/pages/form_page.html"
+
+
+class FormPageField(CoderedFormField):
+    """
+    A field that links to a FormPage.
+    """
+
+    class Meta:
+        ordering = ["sort_order"]
+
+    page = ParentalKey("FormPage", related_name="form_fields")
+
+
+class FormConfirmEmail(CoderedEmail):
+    """
+    Sends a confirmation email after submitting a FormPage.
+    """
+
+    page = ParentalKey("FormPage", related_name="confirmation_emails")
+
+
+class LocationPage(CoderedLocationPage):
+    """
+    A page that holds a location.  This could be a store, a restaurant, etc.
+    """
+
+    class Meta:
+        verbose_name = "Location Page"
+
+    template = "coderedcms/pages/location_page.html"
+
+    # Only allow LocationIndexPages above this page.
+    parent_page_types = ["website.LocationIndexPage"]
+
+
+class LocationIndexPage(CoderedLocationIndexPage):
+    """
+    A page that holds a list of locations and displays them with a Google Map.
+    This does require a Google Maps API Key in Settings > CRX Settings
+    """
+
+    class Meta:
+        verbose_name = "Location Landing Page"
+
+    # Override to specify custom index ordering choice/default.
+    index_query_pagemodel = "website.LocationPage"
+
+    # Only allow LocationPages beneath this page.
+    subpage_types = ["website.LocationPage"]
+
+    template = "coderedcms/pages/location_index_page.html"
+
+
+class WebPage(CoderedWebPage):
+    """
+    General use page with featureful streamfield and SEO attributes.
+    """
+
+    class Meta:
+        verbose_name = "Web Page"
+
+    template = "coderedcms/pages/web_page.html"

+ 0 - 0
coderedcms/project_template/pro/website/static/website/js/custom.js


+ 0 - 0
coderedcms/project_template/sass/website/static/website/src/_variables.scss → coderedcms/project_template/pro/website/static/website/src/_variables.scss


+ 0 - 0
coderedcms/project_template/sass/website/static/website/src/custom.scss → coderedcms/project_template/pro/website/static/website/src/custom.scss


+ 0 - 0
coderedcms/project_template/sass/website/templates/coderedcms/pages/base.html → coderedcms/project_template/pro/website/templates/coderedcms/pages/base.html


+ 0 - 2
coderedcms/project_template/sass/requirements-dev.txt

@@ -1,2 +0,0 @@
-# Tooling for local development
-django-sass

+ 0 - 86
coderedcms/project_template/sass/website/models.py

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

+ 2 - 2
coderedcms/tests/test_bin.py

@@ -88,7 +88,7 @@ class TestCoderedcmsStart(unittest.TestCase):
         )
         self.cleanup()
 
-    def test_template_sass(self):
+    def test_template_pro(self):
         self.setup()
         # Set args
         sys.argv = [
@@ -97,7 +97,7 @@ class TestCoderedcmsStart(unittest.TestCase):
             "myproject",
             self.TEST_DIR,
             "--template",
-            "sass",
+            "pro",
         ]
         # Run
         coderedcms_main()

+ 29 - 18
docs/getting_started/install.rst

@@ -7,23 +7,20 @@ Basic Installation
 
 #. Make a directory (folder) for your project.
 #. Create a virtual environment.
-    **Not sure how to create a virtual environment?**
-    Creating a virtual environment for your project only involves a few commands.
-    See below:
 
     **Windows (PowerShell):**
 
     .. code-block:: ps1con
 
-        PS> python -m venv .\venv\
-        PS> .\venv\Scripts\Activate.ps1
+       PS> python -m venv .\venv\
+       PS> .\venv\Scripts\Activate.ps1
 
     **macOS, Linux:**
 
     .. code-block:: console
 
-        $ python -m venv ./venv/
-        $ source ./venv/bin/activate
+       $ python -m venv ./venv/
+       $ source ./venv/bin/activate
 
     You can name your virtual environment anything you like. It is just for your use
     on your computer.
@@ -32,8 +29,9 @@ Basic Installation
     environments here <https://docs.python.org/3/tutorial/venv.html>`_.
 
     .. note::
-        You will need to be in the directory (folder) of your Wagtail project and have your
-        virtual environment activated to install dependencies and run your site.
+
+       You will need to be in the directory (folder) of your Wagtail project and have your
+       virtual environment activated to install dependencies and run your site.
 
 #. Run ``pip install coderedcms``
 #. Run ``coderedcms start mysite --sitename "My Company Inc." --domain www.example.com``
@@ -55,20 +53,28 @@ Follow the tutorial to build: :doc:`tutorial01`.
 You can also play around with our tutorial database. Learn more: :ref:`load-data`.
 
 
-Installing with Sass Support
-----------------------------
+Professional Installation (includes Sass/SCSS)
+----------------------------------------------
+
+The professional boilerplate includes additional features pre-configured, such as:
+
+* Custom Image and Document models
+* Custom User model (using email address as username)
+* SCSS compilation (using Python, not Node.js)
+* Ruff, MyPy, Pytest tooling pre-configured
 
-To create a project that is pre-configured to use Sass for CSS compilation:
+To use the professional boilerplate, add ``--template pro`` to the start command:
 
 #. Run ``pip install coderedcms``
 #. Run
 
    .. code-block:: console
 
-       $ coderedcms start mysite --template sass --sitename "My Company Inc." --domain www.example.com
+      $ coderedcms start mysite --template pro --sitename "My Company Inc." --domain www.example.com
 
    .. note::
-       ``--sitename`` and ``--domain`` are optional to pre-populate settings of your website.
+
+      ``--sitename`` and ``--domain`` are optional to pre-populate settings of your website.
 
 #. Enter the ``mysite`` project with ``cd mysite/``.
 #. Install the development tooling with:
@@ -109,7 +115,7 @@ the project and play around with it. The database is located in ``website > fixt
 
 Follow these steps to upload it:
 
-1. Navigate to the tutorial project in the Command Line by going to ``coderedcms > tutorial > mysite``. 
+1. Navigate to the tutorial project in the Command Line by going to ``coderedcms > tutorial > mysite``.
 
 2. In a fresh virtual environment, type ``pip install -r requirements.txt`` to set up the requirements for the project.
 
@@ -117,7 +123,7 @@ Follow these steps to upload it:
 
 4. Do the initial migration for the tutorial site with ``python manage.py migrate``.
 
-5. Navigate to the ``database.json`` file in the Fixtures folder and copy the path to the file. 
+5. Navigate to the ``database.json`` file in the Fixtures folder and copy the path to the file.
 
 6. From the Command Line, type ``python manage.py loaddata "path/to/database.json"``, replacing that last part with the correct path to the file.
 
@@ -136,6 +142,11 @@ or at a URL using the ``--template`` option. Additionally, we provide some built
 | ``basic``  | The default starter project. The simplest option, good for most |
 |            | sites.                                                          |
 +------------+-----------------------------------------------------------------+
-| ``sass``   | Similar to basic, but with extra tooling to support SCSS to CSS |
-|            | compilation.                                                    |
+| ``pro``    | Custom Image, Document, User models. Extra tooling to support   |
+|            | SCSS to CSS compilation. Developer tooling such as ruff, mypy,  |
+|            | and pytest.                                                     |
 +------------+-----------------------------------------------------------------+
+
+.. versionchanged:: 3.0
+
+   The "pro" template was added in version 3.0. Previously it was named "sass" and had fewer features.

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff