├── .all-contributorsrc ├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── renovate.json └── workflows │ └── main.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .pyup.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── cookiecutter.json ├── docs └── img │ └── screenshot.png ├── hooks ├── post_gen_project.py └── pre_gen_project.py ├── package-lock.json ├── package.json ├── pyproject.toml ├── requirements.txt ├── setup.cfg ├── tasks.py ├── tests └── test_cookiecutter_generation.py └── {{cookiecutter.project_slug}} ├── #vscode └── settings.json ├── .babelrc.js ├── .dockerignore ├── .editorconfig ├── .env.sample ├── .eslintrc ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── renovate.json └── workflows │ ├── gh-pages.yml │ ├── greetings.yml │ └── main.yml ├── .gitignore ├── .gitlab-ci.yml ├── .nvmrc ├── .pre-commit-config.yaml ├── .prettierignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Procfile ├── README.md ├── app.json ├── bin └── post_compile ├── docker-compose.yml ├── docker ├── Procfile ├── README.md ├── db │ ├── Dockerfile │ └── create.sql └── docker-compose-frontend.yml ├── docs ├── architecture.md ├── configuration.md ├── deployment.md ├── dev-setup.md ├── frontend.md ├── images │ ├── architecture.excalidraw │ ├── architecture.png │ └── architecture.svg ├── index.md ├── introduction.md ├── maintenance.md ├── project-structure.md ├── references.md └── testing.md ├── manage.py ├── mkdocs.yml ├── package-lock.json ├── package.json ├── poetry.lock ├── pyproject.toml ├── tasks.py ├── webpack.config.js └── {{cookiecutter.project_slug}} ├── __init__.py ├── assets ├── ico │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ └── site.webmanifest ├── img │ ├── fake-logo.svg │ ├── search.svg │ └── warning.svg ├── js │ └── main.js └── scss │ ├── abstracts │ ├── _custom_bootstrap_vars.scss │ └── _variables.scss │ ├── base │ ├── _base.scss │ └── _typography.scss │ ├── components │ └── _navbar.scss │ ├── layout │ ├── _footer.scss │ └── _header.scss │ ├── main.scss │ ├── pages │ └── _home.scss │ ├── themes │ └── _dark.scss │ └── vendors │ └── _wagtail.scss ├── conftest.py ├── core ├── __init__.py ├── apps.py ├── templates │ └── core │ │ └── search.html ├── tests │ ├── __init__.py │ └── test_search.py ├── views.py └── wagtail_hooks.py ├── files └── .gitignore ├── home ├── __init__.py ├── apps.py ├── factories.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── templates │ └── home │ │ └── home_page.html └── tests │ ├── __init__.py │ ├── test_factories.py │ └── test_models.py ├── settings ├── __init__.py ├── base.py ├── dev.py ├── production.py └── test.py ├── templates ├── 400.html ├── 403.html ├── 404.html ├── 500.html ├── base.html ├── includes │ ├── footer.html │ └── navbar.html └── wagtailadmin │ ├── home.html │ └── login.html ├── urls.py ├── users ├── __init__.py ├── admin.py ├── apps.py ├── factories.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py └── tests │ ├── __init__.py │ └── test_factories.py └── wsgi.py /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "cookiecutter-wagtail-vix", 3 | "projectOwner": "engineervix" 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | insert_final_newline = true 4 | trim_trailing_whitespace = true 5 | charset = utf-8 6 | 7 | [*.py,BUILD] 8 | indent_style = space 9 | indent_size = 4 10 | 11 | [*.{js,jsx,less,html,json,css,scss,haml,yaml,yml,coffee,sh,sql}] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [{Makefile,*.mk}] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Autodetect text files 2 | * text=auto 3 | 4 | # Ensure those won't be messed up with 5 | *.svg binary 6 | *.jpg binary 7 | *.png binary 8 | *.pdf binary 9 | *.pptx binary 10 | *.docx binary 11 | *.xlsx binary 12 | *.ppt binary 13 | *.doc binary 14 | *.xls binary 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * application version (git tag or commit SHA): 2 | * Python version: 3 | * Operating System: 4 | 5 | ### Description 6 | 7 | Describe what you were trying to get done. 8 | Tell us what happened, what went wrong, and what you expected to happen. 9 | 10 | ### What I Did 11 | 12 | ``` 13 | Paste the command(s) you ran and the output. 14 | If there was a crash, please include the traceback here. 15 | ``` 16 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "pin": { 4 | "automerge": true, 5 | "labels": ["dependencies"] 6 | }, 7 | "packageRules": [ 8 | { 9 | "matchLanguages": ["python"], 10 | "labels": ["dependencies", "python"], 11 | "updateTypes": ["minor", "patch", "pin", "digest"], 12 | "automerge": true, 13 | "semanticCommitType": "build" 14 | }, 15 | { 16 | "matchDepTypes": ["devDependencies"], 17 | "automerge": true, 18 | "labels": ["dependencies"] 19 | }, 20 | { 21 | "matchLanguages": ["javascript"], 22 | "matchDepTypes": ["dependencies"], 23 | "labels": ["dependencies", "javascript"], 24 | "updateTypes": ["minor", "patch", "pin", "digest"], 25 | "automerge": true, 26 | "semanticCommitType": "build" 27 | }, 28 | { 29 | "matchManagers": ["pre-commit"], 30 | "updateTypes": ["minor", "patch", "pin", "digest"], 31 | "automerge": true, 32 | "semanticCommitType": "ci" 33 | }, 34 | { 35 | "matchDatasources": ["docker"], 36 | "updateTypes": ["patch", "pin", "digest"], 37 | "automerge": true, 38 | "semanticCommitType": "ci" 39 | }, 40 | { 41 | "matchManagers": ["circleci", "github-actions"], 42 | "updateTypes": ["patch", "pin", "digest"], 43 | "automerge": true, 44 | "semanticCommitType": "ci" 45 | }, 46 | { 47 | "matchPackageNames": ["wagtail"], 48 | "matchLanguages": ["python"], 49 | "updateTypes": ["minor"], 50 | "automerge": false, 51 | "semanticCommitType": "build" 52 | }, 53 | { 54 | "matchPackageNames": ["sass"], 55 | "enabled": false 56 | } 57 | ], 58 | "timezone": "Africa/Lusaka", 59 | "schedule": ["every weekend"] 60 | } 61 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | # Enable Buildkit and let compose use it to speed up image building 4 | env: 5 | DOCKER_BUILDKIT: 1 6 | COMPOSE_DOCKER_CLI_BUILD: 1 7 | 8 | on: 9 | pull_request: 10 | branches: ["main"] 11 | push: 12 | branches: ["main"] 13 | tags: 14 | - "v*" 15 | 16 | jobs: 17 | test: 18 | runs-on: ubuntu-22.04 19 | 20 | steps: 21 | - name: Checkout Code Repository 22 | uses: actions/checkout@v4 23 | 24 | - name: Set up Python 25 | uses: actions/setup-python@v5 26 | with: 27 | python-version: "3.12" 28 | cache: "pip" # caching pip dependencies 29 | 30 | - name: Install dependencies 31 | run: | 32 | python -m venv venv 33 | source venv/bin/activate 34 | pip install -r requirements.txt 35 | 36 | - name: Run tests 37 | run: | 38 | source venv/bin/activate 39 | flake8 --exclude=venv* --statistics --exit-zero 40 | pytest 41 | 42 | - name: Upload coverage reports to Codecov 43 | uses: codecov/codecov-action@v4 44 | with: 45 | fail_ci_if_error: true 46 | token: ${{ secrets.CODECOV_TOKEN }} 47 | 48 | # Creates a GitHub Release when the test job succeeds, and only on pushes to tags. 49 | release: 50 | needs: [test] 51 | 52 | permissions: 53 | contents: write 54 | 55 | if: needs.test.result == 'success' && startsWith( github.ref, 'refs/tags/v' ) 56 | 57 | runs-on: ubuntu-22.04 58 | 59 | steps: 60 | - name: Check out the repo 61 | uses: actions/checkout@v4 62 | - name: Set up Python 63 | uses: actions/setup-python@v5 64 | with: 65 | python-version: "3.12" 66 | - name: Install dependencies 67 | run: | 68 | python -m venv venv 69 | source venv/bin/activate 70 | pip install --upgrade pip 71 | pip install invoke colorama 72 | - name: Get the version 73 | id: get_version 74 | run: | 75 | echo "${{ github.ref }}" 76 | echo "VERSION=$(echo $GITHUB_REF | sed 's/refs\/tags\///')" >> $GITHUB_ENV 77 | - name: Generate Release Title 78 | id: get_release_title 79 | shell: bash 80 | run: | 81 | export TODAY="($(TZ=Africa/Lusaka date --iso))" 82 | echo "RELEASE_NAME=$VERSION $TODAY" >> $GITHUB_ENV 83 | - name: Extract Release Notes 84 | # This creates a file LATEST_RELEASE_NOTES.md in the parent directory (../) 85 | shell: bash 86 | run: | 87 | source venv/bin/activate 88 | invoke get-release-notes 89 | - name: GitHub Release 90 | uses: softprops/action-gh-release@v1 91 | with: 92 | name: ${{ env.RELEASE_NAME }} 93 | body_path: ../LATEST_RELEASE_NOTES.md 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/python,sublimetext,vim,emacs,git,pycharm,macos,django,node,visualstudiocode 3 | 4 | ### Django ### 5 | *.log 6 | *.pot 7 | *.pyc 8 | __pycache__/ 9 | local_settings.py 10 | db.sqlite3 11 | media 12 | 13 | # If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ 14 | # in your Git repository. Update and uncomment the following line accordingly. 15 | staticfiles/ 16 | 17 | ### Emacs ### 18 | # -*- mode: gitignore; -*- 19 | *~ 20 | \#*\# 21 | /.emacs.desktop 22 | /.emacs.desktop.lock 23 | *.elc 24 | auto-save-list 25 | tramp 26 | .\#* 27 | 28 | # Org-mode 29 | .org-id-locations 30 | *_archive 31 | 32 | # flymake-mode 33 | *_flymake.* 34 | 35 | # eshell files 36 | /eshell/history 37 | /eshell/lastdir 38 | 39 | # elpa packages 40 | /elpa/ 41 | 42 | # reftex files 43 | *.rel 44 | 45 | # AUCTeX auto folder 46 | /auto/ 47 | 48 | # cask packages 49 | .cask/ 50 | dist/ 51 | 52 | # Flycheck 53 | flycheck_*.el 54 | 55 | # server auth directory 56 | /server/ 57 | 58 | # projectiles files 59 | .projectile 60 | projectile-bookmarks.eld 61 | 62 | # directory configuration 63 | .dir-locals.el 64 | 65 | # saveplace 66 | places 67 | 68 | # url cache 69 | url/cache/ 70 | 71 | # cedet 72 | ede-projects.el 73 | 74 | # smex 75 | smex-items 76 | 77 | # company-statistics 78 | company-statistics-cache.el 79 | 80 | # anaconda-mode 81 | anaconda-mode/ 82 | 83 | ### Git ### 84 | *.orig 85 | 86 | ### macOS ### 87 | *.DS_Store 88 | .AppleDouble 89 | .LSOverride 90 | 91 | # Icon must end with two \r 92 | Icon 93 | 94 | # Thumbnails 95 | ._* 96 | 97 | # Files that might appear in the root of a volume 98 | .DocumentRevisions-V100 99 | .fseventsd 100 | .Spotlight-V100 101 | .TemporaryItems 102 | .Trashes 103 | .VolumeIcon.icns 104 | .com.apple.timemachine.donotpresent 105 | 106 | # Directories potentially created on remote AFP share 107 | .AppleDB 108 | .AppleDesktop 109 | Network Trash Folder 110 | Temporary Items 111 | .apdisk 112 | 113 | ### Node ### 114 | # Logs 115 | logs 116 | npm-debug.log* 117 | yarn-debug.log* 118 | yarn-error.log* 119 | 120 | # Runtime data 121 | pids 122 | *.pid 123 | *.seed 124 | *.pid.lock 125 | 126 | # Directory for instrumented libs generated by jscoverage/JSCover 127 | lib-cov 128 | 129 | # Coverage directory used by tools like istanbul 130 | coverage 131 | 132 | # nyc test coverage 133 | .nyc_output 134 | 135 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 136 | .grunt 137 | 138 | # Bower dependency directory (https://bower.io/) 139 | bower_components 140 | 141 | # node-waf configuration 142 | .lock-wscript 143 | 144 | # Compiled binary addons (http://nodejs.org/api/addons.html) 145 | build/Release 146 | 147 | # Dependency directories 148 | node_modules/ 149 | jspm_packages/ 150 | 151 | # Typescript v1 declaration files 152 | typings/ 153 | 154 | # Optional npm cache directory 155 | .npm 156 | 157 | # Optional eslint cache 158 | .eslintcache 159 | 160 | # Optional REPL history 161 | .node_repl_history 162 | 163 | # Output of 'npm pack' 164 | *.tgz 165 | 166 | # Yarn Integrity file 167 | .yarn-integrity 168 | 169 | # dotenv environment variables file 170 | .env 171 | 172 | ### PyCharm ### 173 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 174 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 175 | 176 | # User-specific stuff: 177 | .idea/**/workspace.xml 178 | .idea/**/tasks.xml 179 | .idea/dictionaries 180 | 181 | # Sensitive or high-churn files: 182 | .idea/**/dataSources/ 183 | .idea/**/dataSources.ids 184 | .idea/**/dataSources.xml 185 | .idea/**/dataSources.local.xml 186 | .idea/**/sqlDataSources.xml 187 | .idea/**/dynamic.xml 188 | .idea/**/uiDesigner.xml 189 | 190 | # Gradle: 191 | .idea/**/gradle.xml 192 | .idea/**/libraries 193 | 194 | # CMake 195 | cmake-build-debug/ 196 | 197 | # Mongo Explorer plugin: 198 | .idea/**/mongoSettings.xml 199 | 200 | ## File-based project format: 201 | *.iws 202 | 203 | ## Plugin-specific files: 204 | 205 | # IntelliJ 206 | /out/ 207 | 208 | # mpeltonen/sbt-idea plugin 209 | .idea_modules/ 210 | 211 | # JIRA plugin 212 | atlassian-ide-plugin.xml 213 | 214 | # Cursive Clojure plugin 215 | .idea/replstate.xml 216 | 217 | # Ruby plugin and RubyMine 218 | /.rakeTasks 219 | 220 | # Crashlytics plugin (for Android Studio and IntelliJ) 221 | com_crashlytics_export_strings.xml 222 | crashlytics.properties 223 | crashlytics-build.properties 224 | fabric.properties 225 | 226 | ### PyCharm Patch ### 227 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 228 | 229 | # *.iml 230 | # modules.xml 231 | # .idea/misc.xml 232 | # *.ipr 233 | 234 | # Sonarlint plugin 235 | .idea/sonarlint 236 | 237 | ### Python ### 238 | # Byte-compiled / optimized / DLL files 239 | *.py[cod] 240 | *$py.class 241 | 242 | # C extensions 243 | *.so 244 | 245 | # Distribution / packaging 246 | .Python 247 | build/ 248 | develop-eggs/ 249 | downloads/ 250 | eggs/ 251 | .eggs/ 252 | lib/ 253 | lib64/ 254 | parts/ 255 | sdist/ 256 | var/ 257 | wheels/ 258 | *.egg-info/ 259 | .installed.cfg 260 | *.egg 261 | 262 | # PyInstaller 263 | # Usually these files are written by a python script from a template 264 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 265 | *.manifest 266 | *.spec 267 | 268 | # Installer logs 269 | pip-log.txt 270 | pip-delete-this-directory.txt 271 | 272 | # Unit test / coverage reports 273 | htmlcov/ 274 | .tox/ 275 | .coverage 276 | .coverage.* 277 | .cache 278 | .pytest_cache/ 279 | nosetests.xml 280 | coverage.* 281 | *.cover 282 | .hypothesis/ 283 | 284 | # Translations 285 | *.mo 286 | 287 | # Flask stuff: 288 | instance/ 289 | .webassets-cache 290 | 291 | # Scrapy stuff: 292 | .scrapy 293 | 294 | # Sphinx documentation 295 | docs/_build/ 296 | 297 | # PyBuilder 298 | target/ 299 | 300 | # Jupyter Notebook 301 | .ipynb_checkpoints 302 | 303 | # pyenv 304 | .python-version 305 | 306 | # celery beat schedule file 307 | celerybeat-schedule.* 308 | 309 | # SageMath parsed files 310 | *.sage.py 311 | 312 | # Environments 313 | .venv 314 | env/ 315 | venv/ 316 | ENV/ 317 | env.bak/ 318 | venv.bak/ 319 | 320 | # Spyder project settings 321 | .spyderproject 322 | .spyproject 323 | 324 | # Rope project settings 325 | .ropeproject 326 | 327 | # mkdocs documentation 328 | /site 329 | 330 | # mypy 331 | .mypy_cache/ 332 | 333 | ### SublimeText ### 334 | # cache files for sublime text 335 | *.tmlanguage.cache 336 | *.tmPreferences.cache 337 | *.stTheme.cache 338 | 339 | # workspace files are user-specific 340 | *.sublime-workspace 341 | 342 | # project files should be checked into the repository, unless a significant 343 | # proportion of contributors will probably not be using SublimeText 344 | # *.sublime-project 345 | 346 | # sftp configuration file 347 | sftp-config.json 348 | 349 | # Package control specific files 350 | Package Control.last-run 351 | Package Control.ca-list 352 | Package Control.ca-bundle 353 | Package Control.system-ca-bundle 354 | Package Control.cache/ 355 | Package Control.ca-certs/ 356 | Package Control.merged-ca-bundle 357 | Package Control.user-ca-bundle 358 | oscrypto-ca-bundle.crt 359 | bh_unicode_properties.cache 360 | 361 | # Sublime-github package stores a github token in this file 362 | # https://packagecontrol.io/packages/sublime-github 363 | GitHub.sublime-settings 364 | 365 | ### Vim ### 366 | # swap 367 | .sw[a-p] 368 | .*.sw[a-p] 369 | # session 370 | Session.vim 371 | # temporary 372 | .netrwhist 373 | # auto-generated tag files 374 | tags 375 | 376 | ### VisualStudioCode ### 377 | .vscode/* 378 | .vscode/settings.json 379 | !.vscode/tasks.json 380 | !.vscode/launch.json 381 | !.vscode/extensions.json 382 | .history 383 | 384 | 385 | # End of https://www.gitignore.io/api/python,sublimetext,vim,emacs,git,pycharm,macos,django,node,visualstudiocode 386 | 387 | # Webpack 388 | webpack-stats.json 389 | 390 | # Custom Pycharm overrides 391 | .idea 392 | 393 | mediafiles/ 394 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.5.0 6 | hooks: 7 | - id: check-added-large-files 8 | args: ["--maxkb=5000"] 9 | - id: end-of-file-fixer 10 | - id: check-case-conflict 11 | - id: detect-private-key 12 | - id: check-docstring-first 13 | - repo: https://github.com/psf/black 14 | rev: 24.1.1 15 | hooks: 16 | - id: black 17 | exclude: | 18 | (?x)( 19 | ^{{cookiecutter.project_slug}}/ 20 | |(.*)/migrations 21 | ) 22 | - repo: https://github.com/PyCQA/flake8 23 | rev: 7.0.0 24 | hooks: 25 | - id: flake8 26 | exclude: | 27 | (?x)( 28 | ^{{cookiecutter.project_slug}}/ 29 | ) 30 | - repo: https://github.com/commitizen-tools/commitizen 31 | rev: v3.14.1 32 | hooks: 33 | - id: commitizen 34 | stages: [commit-msg] 35 | -------------------------------------------------------------------------------- /.pyup.yml: -------------------------------------------------------------------------------- 1 | # autogenerated pyup.io config file 2 | # see https://pyup.io/docs/configuration/ for all available options 3 | 4 | schedule: '' 5 | update: false 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution guide 2 | 3 | First of all, thank you for taking the time to contribute! 🎉 4 | 5 | When contributing to [this project](https://github.com/engineervix/cookiecutter-wagtail-vix), please first create an [issue](https://github.com/engineervix/cookiecutter-wagtail-vix/issues) to discuss the change you wish to make before sending a pull request. 6 | 7 | If you are proposing a feature: 8 | 9 | - Explain in detail how it would work. 10 | - Keep the scope as narrow as possible, to make it easier to implement. 11 | - Remember that this is a volunteer-driven project, and that contributions are welcome 😊. 12 | 13 | ## Before making a pull request 14 | 15 | 1. Follow the instructions in the **Contributing** section of the [README.md](https://github.com/engineervix/cookiecutter-wagtail-vix#contributing) 16 | 2. Check out a new branch and add your modification. 17 | 3. If possible, write tests, and ensure that they pass. This project uses [unittest](https://docs.python.org/3/library/unittest.html). 18 | 4. Update `README.md` for your changes. Please make sure that the Table of Contents is up to date. [DocToc](https://github.com/thlorenz/doctoc) makes this pretty easy, as you simply run `doctoc README.md` (You can install it globally on your system via `npm install -g doctoc`). 19 | 5. Commit your changes. If you do it via `cz commit` or `npm run commit`, you'll get some fancy prompts to help you to appropriately categorize your commit. 20 | 6. Send a [pull request](https://github.com/engineervix/cookiecutter-wagtail-vix/pulls) 🙏 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2024 Victor Miti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cookiecutter.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_name": "Project Name", 3 | "project_slug": "{{ cookiecutter.project_name.lower()|replace(' ', '_')|replace('-', '_')|replace('.', '_')|trim() }}", 4 | "description": "A wagtail powered project.", 5 | "domain_name": "example.com", 6 | "timezone": "Africa/Lusaka", 7 | "author_name": "Victor Miti", 8 | "github_username": "engineervix", 9 | "email": "{{ cookiecutter.author_name.lower()|replace(' ', '-') }}@example.com" 10 | } 11 | -------------------------------------------------------------------------------- /docs/img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engineervix/cookiecutter-wagtail-vix/66d5ca64237bd487092b2a65986a9254a693a008/docs/img/screenshot.png -------------------------------------------------------------------------------- /hooks/post_gen_project.py: -------------------------------------------------------------------------------- 1 | import os 2 | import secrets 3 | import shutil 4 | 5 | # Get the root project directory 6 | PROJECT_DIRECTORY = os.path.realpath(os.path.curdir) 7 | 8 | 9 | def generate_secret_key(config_file_location): 10 | # Open file 11 | with open(config_file_location) as f: 12 | file_ = f.read() 13 | 14 | # Generate a SECRET_KEY 15 | SECRET_KEY = secrets.token_hex(25) 16 | 17 | file_ = file_.replace( 18 | "DJANGO_SECRET_KEY=secret", 19 | f"DJANGO_SECRET_KEY={SECRET_KEY}", 20 | ) 21 | 22 | # Write the results to file 23 | with open(config_file_location, "w") as f: 24 | f.write(file_) 25 | 26 | 27 | def set_secret_key(project_directory): 28 | example_dotenv_file = os.path.join(project_directory, ".env.sample") 29 | dotenv_file = os.path.join(project_directory, ".env") 30 | 31 | shutil.copy(example_dotenv_file, dotenv_file) 32 | 33 | generate_secret_key(dotenv_file) 34 | 35 | 36 | def main(): 37 | # Generate and set random secret key 38 | set_secret_key(PROJECT_DIRECTORY) 39 | 40 | # rename directory so that it's not included in version control 41 | vscode = os.path.join(PROJECT_DIRECTORY, "#vscode/") 42 | 43 | # the renamed version is in the .gitignore file 44 | shutil.move(vscode, os.path.join(PROJECT_DIRECTORY, ".vscode/")) 45 | 46 | 47 | if __name__ == "__main__": 48 | main() 49 | -------------------------------------------------------------------------------- /hooks/pre_gen_project.py: -------------------------------------------------------------------------------- 1 | project_slug = "{{ cookiecutter.project_slug }}" 2 | if hasattr(project_slug, "isidentifier"): 3 | assert ( 4 | project_slug.isidentifier() 5 | ), "'{}' project slug is not a valid Python identifier.".format(project_slug) 6 | 7 | assert ( 8 | project_slug == project_slug.lower() 9 | ), "'{}' project slug should be all lowercase".format(project_slug) 10 | 11 | assert ( 12 | "\\" not in "{{ cookiecutter.author_name }}" 13 | ), "Don't include backslashes in author name." 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cookiecutter-wagtail-vix", 3 | "version": "2024.07.19", 4 | "description": "a minimal, batteries-included, reusable project skeleton to serve as a starting point for a Wagtail project", 5 | "scripts": { 6 | "test": "pytest", 7 | "release": "commit-and-tag-version" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/engineervix/cookiecutter-wagtail-vix.git" 12 | }, 13 | "keywords": [ 14 | "cookiecutter", 15 | "wagtail", 16 | "django", 17 | "CMS" 18 | ], 19 | "author": "Victor Miti", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/engineervix/cookiecutter-wagtail-vix/issues" 23 | }, 24 | "homepage": "https://github.com/engineervix/cookiecutter-wagtail-vix#readme", 25 | "devDependencies": { 26 | "commit-and-tag-version": "^12.4.1" 27 | }, 28 | "commit-and-tag-version": { 29 | "header": "# Changelog\n\nAll notable changes to this project will be documented here.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project uses ~~[Semantic Versioning](https://semver.org/spec/v2.0.0.html)~~ [Calendar Versioning](https://calver.org/).\n", 30 | "types": [ 31 | { 32 | "type": "feat", 33 | "section": "🚀 Features" 34 | }, 35 | { 36 | "type": "fix", 37 | "section": "🐛 Bug Fixes" 38 | }, 39 | { 40 | "type": "docs", 41 | "section": "📝 Docs", 42 | "hidden": false 43 | }, 44 | { 45 | "type": "style", 46 | "section": "💄 Styling", 47 | "hidden": false 48 | }, 49 | { 50 | "type": "refactor", 51 | "hidden": false, 52 | "section": "♻️ Code Refactoring" 53 | }, 54 | { 55 | "type": "perf", 56 | "section": "⚡️ Performance Improvements", 57 | "hidden": false 58 | }, 59 | { 60 | "type": "test", 61 | "section": "✅ Tests", 62 | "hidden": false 63 | }, 64 | { 65 | "type": "build", 66 | "section": "⚙️ Build System", 67 | "hidden": false 68 | }, 69 | { 70 | "type": "ci", 71 | "section": "👷 CI/CD", 72 | "hidden": false 73 | }, 74 | { 75 | "type": "chore", 76 | "section": "🚧 Others", 77 | "hidden": true 78 | }, 79 | { 80 | "type": "revert", 81 | "section": "⏪️ Reverts", 82 | "hidden": true 83 | } 84 | ], 85 | "scripts": { 86 | "prechangelog": "sed -e '1,6d' -i CHANGELOG.md", 87 | "postchangelog": "sed -e 's/###\\ \\[/##\\ \\[v/g' -i CHANGELOG.md && sed -re 's/##\\ \\[([0-9])/##\\ \\[v\\1/g' -i CHANGELOG.md" 88 | }, 89 | "engines": { 90 | "node": ">= 22 <23", 91 | "npm": ">= 10" 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.black] 6 | line-length = 88 7 | target-version = ['py38', 'py39', 'py310', 'py311', 'py312'] 8 | include = '\.pyi?$' 9 | exclude = ''' 10 | 11 | ( 12 | /( 13 | \.eggs # exclude a few common directories in the 14 | | \.git # root of the project 15 | | \.hg 16 | | \.mypy_cache 17 | | \.tox 18 | | \.venv 19 | | _build 20 | | buck-out 21 | | build 22 | | (.*)/migrations 23 | | dist 24 | | \{\{cookiecutter\.project_slug\}\} 25 | )/ 26 | ) 27 | ''' 28 | 29 | [bumpver] 30 | current_version = "2024.07.19" 31 | version_pattern = "YYYY.0M.0D[-TAG]" 32 | commit = false 33 | tag = false 34 | push = false 35 | 36 | [bumpver.file_patterns] 37 | "package.json" = [ 38 | '"version": "{version}"', 39 | ] 40 | "package-lock.json" = [ 41 | '"version": "{version}"', 42 | ] 43 | "pyproject.toml" = [ 44 | 'current_version = "{version}"', 45 | # 'version = "{version}"', 46 | ] 47 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cookiecutter==2.6.0 2 | sh==2.1.0 3 | 4 | # Code quality 5 | # ---------------------------------------------------------------------------- 6 | black==24.10.0 7 | flake8==7.1.1 8 | flake8-isort==6.1.1 9 | 10 | # Dev 11 | # ---------------------------------------------------------------------------- 12 | bumpver==2023.1129 13 | bpython 14 | commitizen==3.30.1 15 | invoke==2.2.0 16 | pre-commit==3.8.0 17 | 18 | # Testing 19 | # ---------------------------------------------------------------------------- 20 | pytest==8.3.3 21 | pytest-cookies==0.7.0 22 | pytest-cov==5.0.0 23 | pytest-factoryboy==2.7.0 24 | pytest-instafail==0.5.0 25 | pytest-logger==1.1.1 26 | pytest-mock==3.14.0 27 | pytest-sugar==1.0.0 28 | pytest-xdist==3.6.1 29 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # --- Individual linter configuration --------------------------------------- 2 | 3 | [flake8] 4 | ignore = E203, E266, E501, W503, F403, F401 5 | max-line-length = 88 6 | max-complexity = 18 7 | select = B,C,E,F,W,T4,B9 8 | exclude = .git,__pycache__,{{cookiecutter.project_slug}}/,.mypy_cache,.pytest_cache,.tox 9 | 10 | # --- pytest configuration -------------------------------------------------- 11 | 12 | [tool:pytest] 13 | testpaths = tests 14 | addopts = 15 | -v --tb=short --cov-config=setup.cfg --cov --cov-report json --cov-report term-missing:skip-covered 16 | norecursedirs = 17 | .tox .git */migrations/* */static/* docs venv */{{cookiecutter.project_slug}}/* 18 | 19 | # --- Coverage configuration ------------------------------------------------ 20 | 21 | [coverage:run] 22 | omit = 23 | venv/* 24 | {{cookiecutter.project_slug}}/* 25 | tasks.py 26 | 27 | [coverage:report] 28 | skip_covered = True 29 | show_missing = True 30 | -------------------------------------------------------------------------------- /tasks.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path # noqa: F401 3 | 4 | import tomllib 5 | from colorama import Fore, init 6 | from invoke import task 7 | 8 | 9 | @task(help={"fix": "let black and isort format your files"}) 10 | def lint(c, fix=False): 11 | """flake8, black and isort""" 12 | 13 | if fix: 14 | c.run("black .", pty=True) 15 | c.run("isort --profile black .", pty=True) 16 | else: 17 | c.run("black . --check", pty=True) 18 | c.run("isort --check-only --profile black .", pty=True) 19 | c.run("flake8 .", pty=True) 20 | 21 | 22 | # TODO: create a "clean" collection comprising the next two tasks below 23 | 24 | 25 | @task 26 | def clean_pyc(c): 27 | """remove Python file artifacts""" 28 | 29 | c.run("find . -name '*.pyc' -exec rm -f {} +", pty=True) 30 | c.run("find . -name '*.pyo' -exec rm -f {} +", pty=True) 31 | c.run("find . -name '*~' -exec rm -f {} +", pty=True) 32 | c.run("find . -name '__pycache__' -exec rm -fr {} +", pty=True) 33 | 34 | 35 | @task 36 | def clean_test(c): 37 | """remove test and coverage artifacts""" 38 | 39 | c.run("rm -fr .tox/", pty=True) 40 | c.run("rm -f .coverage", pty=True) 41 | c.run("rm -f coverage.xml", pty=True) 42 | c.run("rm -fr htmlcov/", pty=True) 43 | c.run("rm -fr .pytest_cache", pty=True) 44 | 45 | 46 | @task( 47 | help={ 48 | "branch": "The branch against which you wanna bump", 49 | "push": "Push to origin after bumping", 50 | } 51 | ) 52 | def bump(c, branch="main", push=False): 53 | """Use BumpVer & standard-version to bump version and generate changelog 54 | 55 | Run this task when you want to prepare a release. 56 | First we check that there are no unstaged files before running 57 | """ 58 | 59 | init() 60 | 61 | unstaged_str = "not staged for commit" 62 | uncommitted_str = "to be committed" 63 | check = c.run("git status", pty=True) 64 | if unstaged_str not in check.stdout or uncommitted_str not in check.stdout: 65 | get_current_tag = c.run( 66 | "git describe --abbrev=0 --tags `git rev-list --tags --skip=0 --max-count=1`", 67 | pty=True, 68 | ) 69 | previous_tag = get_current_tag.stdout.rstrip() 70 | c.run("bumpver update", pty=True) 71 | 72 | with open("pyproject.toml", "rb") as f: 73 | toml_dict = tomllib.load(f) 74 | version_files = toml_dict["bumpver"]["file_patterns"].keys() 75 | files_to_add = " ".join(list(version_files)) 76 | c.run( 77 | f"git add {files_to_add}", 78 | pty=True, 79 | ) 80 | c.run( 81 | f'npm run release -- --commit-all --skip.bump --releaseCommitMessageFormat "bump: ✈️ {previous_tag} → v{{{{currentTag}}}}"', 82 | pty=True, 83 | ) 84 | if push: 85 | # push to origin 86 | c.run(f"git push --follow-tags origin {branch}", pty=True) 87 | else: 88 | print( 89 | f"{Fore.RED}Sorry mate, please ensure there are no unstaged files before creating a release{Fore.RESET}" 90 | ) 91 | 92 | 93 | @task 94 | def get_release_notes(c): 95 | """extract content from CHANGELOG.md for use in Github/Gitlab Releases 96 | 97 | we read the file and loop through line by line 98 | we wanna extract content beginning from the first Heading 2 text 99 | to the last line before the next Heading 2 text 100 | """ 101 | 102 | pattern_to_match = "## [v" 103 | 104 | count = 0 105 | lines = [] 106 | heading_text = "## What's changed in this release\n" 107 | lines.append(heading_text) 108 | 109 | with open("CHANGELOG.md", "r") as c: 110 | for line in c: 111 | if pattern_to_match in line and count == 0: 112 | count += 1 113 | elif pattern_to_match not in line and count == 1: 114 | lines.append(line) 115 | elif pattern_to_match in line and count == 1: 116 | break 117 | 118 | # home = str(Path.home()) 119 | # release_notes = os.path.join(home, "LATEST_RELEASE_NOTES.md") 120 | release_notes = os.path.join("../", "LATEST_RELEASE_NOTES.md") 121 | with open(release_notes, "w") as f: 122 | print("".join(lines), file=f, end="") 123 | -------------------------------------------------------------------------------- /tests/test_cookiecutter_generation.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | import pytest 5 | from binaryornot.check import is_binary 6 | from cookiecutter.exceptions import FailedHookException 7 | 8 | PATTERN = r"{{(\s?cookiecutter)[.](.*?)}}" 9 | RE_OBJ = re.compile(PATTERN) 10 | 11 | 12 | @pytest.fixture 13 | def context(): 14 | return { 15 | "project_name": "Test Wagtail Project", 16 | "project_slug": "test_wagtail_project", 17 | "description": "A short description of the project.", 18 | "domain_name": "example.com", 19 | "timezone": "Africa/Lusaka", 20 | "author_name": "Test Author", 21 | "github_username": "engineervix", 22 | "email": "somebody@example.com", 23 | } 24 | 25 | 26 | def build_files_list(root_dir): 27 | """Build a list containing absolute paths to the generated files.""" 28 | return [ 29 | os.path.join(dirpath, file_path) 30 | for dirpath, subdirs, files in os.walk(root_dir) 31 | for file_path in files 32 | ] 33 | 34 | 35 | def check_paths(paths): 36 | """Method to check all paths have correct substitutions.""" 37 | # Assert that no match is found in any of the files 38 | for path in paths: 39 | if is_binary(path): 40 | continue 41 | 42 | for line in open(path, "r"): 43 | match = RE_OBJ.search(line) 44 | msg = "cookiecutter variable not replaced in {}" 45 | assert match is None, msg.format(path) 46 | 47 | 48 | def test_project_generation(cookies, context): 49 | """Test that project is generated and fully rendered.""" 50 | result = cookies.bake(extra_context={**context}) 51 | assert result.exit_code == 0 52 | assert result.exception is None 53 | assert result.project.basename == context["project_slug"] 54 | assert result.project.isdir() 55 | 56 | paths = build_files_list(str(result.project)) 57 | assert paths 58 | check_paths(paths) 59 | 60 | 61 | @pytest.mark.parametrize("slug", ["project slug", "Project_Slug"]) 62 | def test_invalid_slug(cookies, context, slug): 63 | """Invalid slug should fail pre-generation hook.""" 64 | context.update({"project_slug": slug}) 65 | 66 | result = cookies.bake(extra_context=context) 67 | 68 | assert result.exit_code != 0 69 | assert isinstance(result.exception, FailedHookException) 70 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/#vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.defaultInterpreterPath": "~/.virtualenvs/{{cookiecutter.project_slug}}/bin/python", 3 | "files.associations": { 4 | "*.html": "django-html" 5 | }, 6 | "[python]": { 7 | "editor.defaultFormatter": "ms-python.black-formatter", 8 | "editor.codeActionsOnSave": { 9 | "source.organizeImports": "explicit" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = (api) => 2 | api.env("production") 3 | ? { 4 | presets: [ 5 | [ 6 | // Only add polyfill in production since 7 | // most use a recent browser when developing 8 | "@babel/env", 9 | { 10 | useBuiltIns: "usage", 11 | corejs: { version: 3 }, 12 | shippedProposals: true, 13 | bugfixes: true, 14 | }, 15 | ], 16 | ], 17 | // parserOpts: { allowReturnOutsideFunction: true }, 18 | } 19 | : { 20 | // Only transforms new dev syntax like optional chaining 21 | // or nullish coalescing 22 | presets: ["@babel/env"], 23 | }; 24 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.dockerignore: -------------------------------------------------------------------------------- 1 | # Django project 2 | /media/ 3 | /static/ 4 | /staticfiles/ 5 | *.sqlite3 6 | 7 | # Python and others 8 | __pycache__ 9 | *.pyc 10 | .DS_Store 11 | *.swp 12 | /venv/ 13 | /tmp/ 14 | node_modules/ 15 | /npm-debug.log 16 | /.idea/ 17 | .vscode 18 | coverage* 19 | .coverage 20 | coverage 21 | htmlcov 22 | .python-version 23 | .git/ 24 | .env 25 | *.env 26 | fly.toml 27 | .pytest_cache 28 | .ruff_cache 29 | 30 | # Distribution / packaging 31 | .Python 32 | env/ 33 | build/ 34 | develop-eggs/ 35 | dist/ 36 | downloads/ 37 | eggs/ 38 | .eggs/ 39 | lib/ 40 | lib64/ 41 | parts/ 42 | sdist/ 43 | var/ 44 | wheels/ 45 | *.egg-info/ 46 | .installed.cfg 47 | *.egg 48 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | insert_final_newline = true 4 | trim_trailing_whitespace = true 5 | charset = utf-8 6 | 7 | [*.{py,html},BUILD] 8 | indent_style = space 9 | indent_size = 4 10 | 11 | [*.{js,jsx,less,json,css,scss,haml,yaml,yml,coffee,sh,sql}] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [{Makefile,*.mk}] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.env.sample: -------------------------------------------------------------------------------- 1 | # Note: No spaces around '=' sign and no quotes for righthand values. 2 | 3 | # Docker 4 | COMPOSE_DOCKER_CLI_BUILD=1 5 | DOCKER_BUILDKIT=1 6 | COMPOSE_PROJECT_NAME={{cookiecutter.project_slug}} 7 | 8 | # General Settings 9 | 10 | # Example commands to quickly generate a new secret key: 11 | # $ openssl rand -hex 50 12 | # $ python -c 'import random; import string; print("".join([random.SystemRandom().choice(string.digits + string.ascii_letters + string.punctuation) for i in range(100)]))' 13 | DJANGO_SECRET_KEY=secret 14 | # DATABASE_URL=postgres://db_user:db_password@host:port/db_name 15 | DATABASE_URL=postgis://wagtail_dev_user:wagtail_dev_password@db:5432/wagtail_dev_db 16 | DEBUG=True 17 | ALLOWED_HOSTS=127.0.0.1,localhost 18 | WAGTAILADMIN_BASE_URL=http://127.0.0.1:8000 19 | 20 | # https://github.com/rq/django-rq 21 | # RQ_QUEUE=redis://redis:6379/0 22 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@babel/eslint-parser", 3 | "extends": ["plugin:prettier/recommended"], 4 | "env": { 5 | "browser": true, 6 | "node": true 7 | }, 8 | "parserOptions": { 9 | "ecmaVersion": 8, 10 | "sourceType": "module", 11 | "requireConfigFile": false 12 | }, 13 | "rules": { 14 | "semi": 2 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.gitattributes: -------------------------------------------------------------------------------- 1 | # Autodetect text files 2 | * text=auto 3 | 4 | # Ensure those won't be messed up with 5 | *.svg binary 6 | *.jpg binary 7 | *.png binary 8 | *.pdf binary 9 | *.pptx binary 10 | *.docx binary 11 | *.xlsx binary 12 | *.ppt binary 13 | *.doc binary 14 | *.xls binary 15 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - project version (git tag or commit SHA): 2 | - Python version: 3 | - Operating System: 4 | 5 | ### Description 6 | 7 | Describe what you were trying to get done. 8 | Tell us what happened, what went wrong, and what you expected to happen. 9 | 10 | ### What I Did 11 | 12 | ``` 13 | Paste the command(s) you ran and the output. 14 | If there was a crash, please include the traceback here. 15 | ``` 16 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "pin": { 4 | "automerge": true, 5 | "labels": ["dependencies"] 6 | }, 7 | "packageRules": [ 8 | { 9 | "matchLanguages": ["python"], 10 | "labels": ["dependencies", "python"], 11 | "updateTypes": ["minor", "patch", "pin", "digest"], 12 | "automerge": true, 13 | "semanticCommitType": "build" 14 | }, 15 | { 16 | "matchDepTypes": ["devDependencies"], 17 | "automerge": true, 18 | "labels": ["dependencies"] 19 | }, 20 | { 21 | "matchLanguages": ["javascript"], 22 | "matchDepTypes": ["dependencies"], 23 | "labels": ["dependencies", "javascript"], 24 | "updateTypes": ["minor", "patch", "pin", "digest"], 25 | "automerge": true, 26 | "semanticCommitType": "build" 27 | }, 28 | { 29 | "matchManagers": ["pre-commit"], 30 | "updateTypes": ["minor", "patch", "pin", "digest"], 31 | "automerge": true, 32 | "semanticCommitType": "ci" 33 | }, 34 | { 35 | "matchDatasources": ["docker"], 36 | "updateTypes": ["patch", "pin", "digest"], 37 | "automerge": true, 38 | "semanticCommitType": "ci" 39 | }, 40 | { 41 | "matchManagers": ["circleci", "github-actions"], 42 | "updateTypes": ["patch", "pin", "digest"], 43 | "automerge": true, 44 | "semanticCommitType": "ci" 45 | }, 46 | { 47 | "matchPackageNames": ["wagtail"], 48 | "matchLanguages": ["python"], 49 | "updateTypes": ["minor"], 50 | "automerge": false, 51 | "semanticCommitType": "build" 52 | } 53 | ], 54 | "timezone": "{{ cookiecutter.timezone }}", 55 | "schedule": ["every weekend"] 56 | } 57 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | # For this to work, please ensure that your repository has Pages enabled and configured to build using GitHub Actions 3 | 4 | on: 5 | pull_request: 6 | branches: ["main"] 7 | paths: 8 | - "docs/**/*" 9 | - "mkdocs.yml" 10 | 11 | push: 12 | branches: ["main"] 13 | paths: 14 | - "docs/**/*" 15 | - "mkdocs.yml" 16 | 17 | # Allows you to run this workflow manually from the Actions tab 18 | workflow_dispatch: 19 | 20 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 21 | permissions: 22 | contents: read 23 | pages: write 24 | id-token: write 25 | 26 | # Allow one concurrent deployment 27 | concurrency: 28 | group: "pages" 29 | cancel-in-progress: true 30 | 31 | jobs: 32 | build: 33 | runs-on: ubuntu-22.04 34 | steps: 35 | - name: Checkout code 36 | uses: actions/checkout@v4 37 | with: 38 | fetch-depth: 0 39 | 40 | - name: Setup Pages 41 | uses: actions/configure-pages@v4 42 | 43 | - name: Install poetry 44 | run: pipx install poetry 45 | 46 | - name: Set up Python 47 | uses: actions/setup-python@v5 48 | with: 49 | python-version: 3.12 50 | cache: "poetry" 51 | 52 | - name: Install dependencies 53 | run: | 54 | poetry config virtualenvs.in-project true 55 | poetry install --only docs --no-root 56 | 57 | - name: Build site (_site directory name is used for Jekyll compatiblity) 58 | run: | 59 | source "$(poetry env info --path)/bin/activate" 60 | mkdocs build --config-file ./mkdocs.yml --site-dir ./_site 61 | - name: Upload artifact 62 | uses: actions/upload-pages-artifact@v3 63 | 64 | deploy: 65 | needs: build 66 | if: github.ref == 'refs/heads/main' 67 | 68 | # Deploy to the github-pages environment 69 | environment: 70 | name: github-pages{% raw %} 71 | url: ${{ steps.deployment.outputs.page_url }}{% endraw %} 72 | 73 | runs-on: ubuntu-22.04 74 | steps: 75 | - name: Deploy Docs to GitHub Pages 76 | id: deployment 77 | uses: actions/deploy-pages@v4 78 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request, issues] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | steps: 12 | - uses: actions/first-interaction@v1 13 | with: 14 | repo-token: {% raw %}${{ secrets.GITHUB_TOKEN }} 15 | issue-message: "Hello @${{ github.actor }}, thank you for submitting an issue! 🙂" 16 | pr-message: "Hello @${{ github.actor }}, thank you submitting a pull request!"{% endraw %} 17 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | # Enable Buildkit and let compose use it to speed up image building 4 | env: 5 | DOCKER_BUILDKIT: 1 6 | COMPOSE_DOCKER_CLI_BUILD: 1 7 | POETRY_VERSION: 1.8.3 # Make sure this matches the Dockerfile 8 | 9 | on: 10 | pull_request: 11 | branches: ["main"] 12 | paths-ignore: ["docs/**"] 13 | 14 | push: 15 | branches: ["main", "staging"] 16 | paths-ignore: ["docs/**"] 17 | tags: 18 | - "v*" 19 | 20 | jobs: 21 | ruff: 22 | runs-on: ubuntu-22.04 23 | 24 | steps: 25 | - name: Checkout Code Repository 26 | uses: actions/checkout@v4 27 | 28 | - name: Set up Python 29 | uses: actions/setup-python@v5 30 | with: 31 | python-version: "3.12" 32 | 33 | - id: poetry-cache 34 | uses: actions/cache@v4 35 | with: 36 | path: .venv 37 | key: {% raw %}${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}{% endraw %} 38 | 39 | - if: steps.poetry-cache.outputs.cache-hit != 'true' 40 | name: Install Poetry and Dependencies 41 | shell: bash 42 | run: | 43 | pip install --upgrade pip 44 | pip install poetry==$POETRY_VERSION 45 | python -m venv .venv 46 | source .venv/bin/activate 47 | poetry install --with dev,test,docs 48 | 49 | - name: ruff 50 | shell: bash 51 | run: | 52 | source .venv/bin/activate 53 | ruff check . --output-format=github 54 | 55 | black: 56 | runs-on: ubuntu-22.04 57 | 58 | steps: 59 | - name: Checkout Code Repository 60 | uses: actions/checkout@v4 61 | 62 | - name: Set up Python 63 | uses: actions/setup-python@v5 64 | with: 65 | python-version: "3.12" 66 | 67 | - id: poetry-cache 68 | uses: actions/cache@v4 69 | with: 70 | path: .venv 71 | key: {% raw %}${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}{% endraw %} 72 | 73 | - if: steps.poetry-cache.outputs.cache-hit != 'true' 74 | name: Install Poetry and Dependencies 75 | shell: bash 76 | run: | 77 | pip install --upgrade pip 78 | pip install poetry==$POETRY_VERSION 79 | python -m venv .venv 80 | source .venv/bin/activate 81 | poetry install --with dev,test,docs 82 | 83 | - name: black 84 | shell: bash 85 | run: | 86 | source .venv/bin/activate 87 | black . --check 88 | 89 | djlint: 90 | runs-on: ubuntu-22.04 91 | 92 | steps: 93 | - name: Checkout Code Repository 94 | uses: actions/checkout@v4 95 | 96 | - name: Set up Python 97 | uses: actions/setup-python@v5 98 | with: 99 | python-version: "3.12" 100 | 101 | - id: poetry-cache 102 | uses: actions/cache@v4 103 | with: 104 | path: .venv 105 | key: {% raw %}${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}{% endraw %} 106 | 107 | - if: steps.poetry-cache.outputs.cache-hit != 'true' 108 | name: Install Poetry and Dependencies 109 | shell: bash 110 | run: | 111 | pip install --upgrade pip 112 | pip install poetry==$POETRY_VERSION 113 | python -m venv .venv 114 | source .venv/bin/activate 115 | poetry install --with dev,test,docs 116 | 117 | - name: djlint 118 | shell: bash 119 | run: | 120 | source .venv/bin/activate 121 | find {{cookiecutter.project_slug}}/ -name '*.html' -o -name '*.mjml' | xargs djlint --check 122 | 123 | stylelint: 124 | runs-on: ubuntu-22.04 125 | 126 | steps: 127 | - name: Checkout Code Repository 128 | uses: actions/checkout@v4 129 | 130 | - uses: actions/setup-node@v4 131 | with: 132 | node-version-file: .nvmrc 133 | 134 | - id: node-cache 135 | uses: actions/cache@v4 136 | with: 137 | path: node_modules 138 | key: {% raw %}${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}{% endraw %} 139 | 140 | - if: steps.node-cache.outputs.cache-hit != 'true' 141 | run: | 142 | npm ci --no-optional --no-audit --progress=false 143 | 144 | - name: Stylelint 145 | run: | 146 | npm run lint:style 147 | 148 | eslint: 149 | runs-on: ubuntu-22.04 150 | 151 | steps: 152 | - name: Checkout Code Repository 153 | uses: actions/checkout@v4 154 | 155 | - uses: actions/setup-node@v4 156 | with: 157 | node-version-file: .nvmrc 158 | 159 | - id: node-cache 160 | uses: actions/cache@v4 161 | with: 162 | path: node_modules 163 | key: {% raw %}${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}{% endraw %} 164 | 165 | - if: steps.node-cache.outputs.cache-hit != 'true' 166 | run: | 167 | npm ci --no-optional --no-audit --progress=false 168 | 169 | - name: ESLint 170 | run: | 171 | npm run lint:js 172 | 173 | prettier: 174 | runs-on: ubuntu-22.04 175 | 176 | steps: 177 | - name: Checkout Code Repository 178 | uses: actions/checkout@v4 179 | 180 | - uses: actions/setup-node@v4 181 | with: 182 | node-version-file: .nvmrc 183 | 184 | - id: node-cache 185 | uses: actions/cache@v4 186 | with: 187 | path: node_modules 188 | key: {% raw %}${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}{% endraw %} 189 | 190 | - if: steps.node-cache.outputs.cache-hit != 'true' 191 | run: | 192 | npm ci --no-optional --no-audit --progress=false 193 | 194 | - name: Prettier 195 | run: | 196 | npm run lint:format 197 | 198 | test: 199 | runs-on: ubuntu-22.04 200 | needs: [ruff, black, stylelint, eslint, prettier, djlint] 201 | 202 | services: 203 | postgres: 204 | image: postgis/postgis:15-3.4 205 | env: 206 | POSTGRES_USER: test_postgres_user 207 | POSTGRES_PASSWORD: custom_pass 208 | POSTGRES_DB: test_postgres_db 209 | # needed because the postgres container does not provide a healthcheck 210 | options: >- 211 | --health-cmd pg_isready 212 | --health-interval 10s 213 | --health-timeout 5s 214 | --health-retries 5 215 | ports: 216 | # Maps tcp port 5432 on service container to the host 217 | - 5432:5432 218 | 219 | env: 220 | # postgres://user:password@host:port/database 221 | DATABASE_URL: "postgres://test_postgres_user:custom_pass@localhost:5432/test_postgres_db" 222 | DJANGO_SECRET_KEY: "secret" 223 | DEBUG: False 224 | ALLOWED_HOSTS: "" 225 | WAGTAILADMIN_BASE_URL: "www.example.com" 226 | 227 | steps: 228 | - name: Checkout Code Repository 229 | uses: actions/checkout@v4 230 | 231 | - name: Set up Python 232 | uses: actions/setup-python@v5 233 | with: 234 | python-version: "3.12" 235 | 236 | - uses: actions/setup-node@v4 237 | with: 238 | node-version-file: .nvmrc 239 | 240 | - id: poetry-cache 241 | uses: actions/cache@v4 242 | with: 243 | path: .venv 244 | key: {% raw %}${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}{% endraw %} 245 | 246 | - id: node-cache 247 | uses: actions/cache@v4 248 | with: 249 | path: node_modules 250 | key: {% raw %}${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}{% endraw %} 251 | 252 | - name: System Dependencies 253 | shell: bash 254 | run: | 255 | sudo apt-get update --yes --quiet 256 | export DEBIAN_FRONTEND=noninteractive 257 | export TZ={{ cookiecutter.timezone }} 258 | sudo apt-get install --yes --quiet --no-install-recommends gdal-bin libgdal-dev libproj-dev libwebp-dev 259 | sudo sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen 260 | sudo locale-gen 261 | sudo ln -fs /usr/share/zoneinfo/{{ cookiecutter.timezone }} /etc/localtime 262 | sudo dpkg-reconfigure tzdata 263 | export LANG=en_US.UTF-8 264 | export LANGUAGE=en_US:en 265 | export LC_ALL=en_US.UTF-8 266 | 267 | - if: steps.poetry-cache.outputs.cache-hit != 'true' 268 | name: Install Poetry and Dependencies 269 | shell: bash 270 | run: | 271 | pip install --upgrade pip 272 | pip install poetry==$POETRY_VERSION 273 | python -m venv .venv 274 | source .venv/bin/activate 275 | poetry install --with dev,test,docs 276 | 277 | - if: steps.node-cache.outputs.cache-hit != 'true' 278 | run: | 279 | npm ci --no-optional --no-audit --progress=false 280 | 281 | - name: Static 282 | shell: bash 283 | run: | 284 | npm run build:prod 285 | source .venv/bin/activate 286 | python manage.py collectstatic --noinput --clear 287 | 288 | - name: Test with pytest 289 | shell: bash 290 | run: | 291 | # Note that you have to activate the virtualenv in every step 292 | # because GitHub actions doesn't preserve the environment 293 | source .venv/bin/activate 294 | 295 | # Run system checks 296 | python manage.py check 297 | 298 | # Check for missing migrations 299 | python manage.py makemigrations --check --noinput 300 | 301 | # Create cache table. 302 | python manage.py createcachetable 303 | 304 | # Run backend tests 305 | pytest 306 | 307 | # Creates a GitHub Release when the lint & python_tests jobs succeeds, and only on pushes to tags. 308 | release: 309 | needs: [ruff, black, stylelint, eslint, prettier, djlint, test] 310 | 311 | permissions: 312 | contents: write 313 | 314 | if: needs.test.result == 'success' && startsWith( github.ref, 'refs/tags/v' ) 315 | 316 | runs-on: ubuntu-22.04 317 | 318 | steps: 319 | - name: Check out the repo 320 | uses: actions/checkout@v4 321 | 322 | - name: Set up Python 323 | uses: actions/setup-python@v5 324 | with: 325 | python-version: "3.12" 326 | 327 | - id: poetry-cache 328 | uses: actions/cache@v4 329 | with: 330 | path: .venv 331 | key: {% raw %}${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}{% endraw %} 332 | 333 | - if: steps.poetry-cache.outputs.cache-hit != 'true' 334 | name: Install Poetry and Dependencies 335 | shell: bash 336 | run: | 337 | pip install --upgrade pip 338 | pip install poetry==$POETRY_VERSION 339 | python -m venv .venv 340 | source .venv/bin/activate 341 | poetry install --with dev,test,docs 342 | 343 | - name: Get the version 344 | id: get_version 345 | run: |{% raw %} 346 | echo "${{ github.ref }}"{% endraw %} 347 | echo "VERSION=$(echo $GITHUB_REF | sed 's/refs\/tags\///')" >> $GITHUB_ENV 348 | - name: Generate Release Title 349 | id: get_release_title 350 | shell: bash 351 | run: | 352 | export TODAY="($(TZ={{ cookiecutter.timezone }} date --iso))" 353 | echo "RELEASE_NAME=$VERSION $TODAY" >> $GITHUB_ENV 354 | - name: Extract Release Notes 355 | # This creates a file LATEST_RELEASE_NOTES.md in the parent directory (../) 356 | shell: bash 357 | run: | 358 | source .venv/bin/activate 359 | invoke get-release-notes 360 | - name: GitHub Release 361 | uses: softprops/action-gh-release@v1 362 | with: 363 | name: {% raw %}${{ env.RELEASE_NAME }}{% endraw %} 364 | body_path: ../LATEST_RELEASE_NOTES.md 365 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | include: 2 | - template: Security/Dependency-Scanning.gitlab-ci.yml 3 | 4 | stages: 5 | - build 6 | - lint 7 | - test 8 | - prepare 9 | - release 10 | - pages 11 | 12 | variables: 13 | POETRY_VERSION: 1.8.3 # Make sure this matches the Dockerfile 14 | 15 | # Test if static assets can be built successfully. 16 | static: 17 | image: node:22.4-bookworm-slim 18 | stage: build 19 | script: 20 | - npm ci --no-optional --no-audit --progress=false 21 | - npm run build:prod 22 | # Saving the job result as an artifact means that the files can be used by 23 | # other jobs. 24 | artifacts: 25 | name: "static-$CI_JOB_ID" 26 | paths: 27 | - ./node_modules 28 | - ./{{cookiecutter.project_slug}}/static 29 | expire_in: 30 mins 30 | 31 | poetry: 32 | # If you update the python image version here, make sure you update all jobs that depend on this 33 | # and the version in the Dockerfile and provision scripts as well 34 | image: python:3.12-slim-bullseye 35 | stage: build 36 | script: 37 | - apt-get update --yes --quiet 38 | - apt-get install --yes --quiet --no-install-recommends 39 | build-essential 40 | libpq-dev 41 | git 42 | - pip install --upgrade pip 43 | - pip install poetry==${POETRY_VERSION} 44 | - python -m venv .venv 45 | - source .venv/bin/activate 46 | - poetry install --with dev,test,docs 47 | artifacts: 48 | name: "poetry-$CI_JOB_ID" 49 | paths: 50 | - ./.venv/ 51 | expire_in: 30 mins 52 | 53 | ruff: 54 | stage: lint 55 | image: python:3.12-slim-bookworm 56 | dependencies: 57 | - poetry 58 | before_script: 59 | - source .venv/bin/activate 60 | script: 61 | - ruff check . --output-format=gitlab 62 | 63 | black: 64 | stage: lint 65 | image: python:3.12-slim-bookworm 66 | dependencies: 67 | - poetry 68 | before_script: 69 | - source .venv/bin/activate 70 | script: 71 | - black . --check 72 | 73 | djlint: 74 | image: python:3.12-slim-bookworm 75 | stage: lint 76 | dependencies: 77 | - poetry 78 | before_script: 79 | - source .venv/bin/activate 80 | script: 81 | - find {{cookiecutter.project_slug}}/ -name '*.html' -o -name '*.mjml' | xargs djlint --check 82 | 83 | stylelint: 84 | stage: lint 85 | image: node:22.4-bookworm-slim 86 | dependencies: 87 | - static 88 | script: 89 | - npm run lint:style 90 | 91 | eslint: 92 | stage: lint 93 | image: node:22.4-bookworm-slim 94 | dependencies: 95 | - static 96 | script: 97 | - npm run lint:js 98 | 99 | prettier: 100 | stage: lint 101 | image: node:22.4-bookworm-slim 102 | dependencies: 103 | - static 104 | script: 105 | - npm run lint:format 106 | 107 | test: 108 | stage: test 109 | image: python:3.12-slim-bookworm 110 | services: 111 | - postgis/postgis:15-3.4 112 | variables: 113 | POSTGRES_DB: test_postgis_db 114 | POSTGRES_USER: test_postgis_user 115 | POSTGRES_PASSWORD: custom_pass 116 | DATABASE_URL: "postgis://test_postgis_user:custom_pass@postgis-postgis/test_postgis_db" 117 | DJANGO_SECRET_KEY: secret 118 | DEBUG: False 119 | ALLOWED_HOSTS: "" 120 | WAGTAILADMIN_BASE_URL: www.example.com 121 | dependencies: 122 | - static 123 | - poetry 124 | before_script: 125 | - export DEBIAN_FRONTEND=noninteractive 126 | - ln -fs /usr/share/zoneinfo/{{ cookiecutter.timezone }} /etc/localtime 127 | - dpkg-reconfigure --frontend noninteractive tzdata 128 | - apt-get update -y --quiet 129 | - apt-get install -y --quiet --no-install-recommends 130 | build-essential 131 | curl 132 | gdal-bin libgdal-dev binutils libproj-dev 133 | git 134 | libjpeg62-turbo-dev 135 | libmagic1 136 | libpq-dev 137 | libwebp-dev 138 | zlib1g-dev 139 | - source .venv/bin/activate 140 | - ./manage.py collectstatic --noinput --clear 141 | script: 142 | # Run system checks 143 | - ./manage.py check 144 | 145 | # Check for missing migrations 146 | - ./manage.py makemigrations --check --noinput 147 | 148 | # Create cache table. 149 | - ./manage.py createcachetable 150 | 151 | # Run back-end tests 152 | - pytest 153 | coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/' 154 | artifacts: 155 | reports: 156 | coverage_report: 157 | coverage_format: cobertura 158 | path: coverage.xml 159 | 160 | prepare: 161 | stage: prepare # This stage must run before the release stage 162 | image: python:3.12-slim-bookworm 163 | rules: 164 | - if: $CI_COMMIT_TAG 165 | dependencies: 166 | - poetry 167 | before_script: 168 | - source .venv/bin/activate 169 | script: 170 | - echo "TODAY=($(TZ={{ cookiecutter.timezone }} date --iso))" >> variables.env # Generate the TODAY environment variable 171 | - invoke get-release-notes 172 | - mv -v ../LATEST_RELEASE_NOTES.md . 173 | artifacts: 174 | paths: 175 | - LATEST_RELEASE_NOTES.md 176 | reports: 177 | dotenv: variables.env 178 | 179 | release: 180 | stage: release 181 | image: registry.gitlab.com/gitlab-org/release-cli:latest 182 | needs: 183 | - job: prepare 184 | artifacts: true 185 | rules: 186 | - if: $CI_COMMIT_TAG # Run this job when a tag is created 187 | script: 188 | - echo "Creating a Gitlab Release for $CI_COMMIT_TAG" 189 | release: # See https://docs.gitlab.com/ee/ci/yaml/#release for available properties 190 | name: "$CI_COMMIT_TAG $TODAY" 191 | description: LATEST_RELEASE_NOTES.md 192 | tag_name: "$CI_COMMIT_TAG" 193 | 194 | # https://docs.gitlab.com/ee/user/project/pages/pages_access_control.html 195 | pages: 196 | image: python:3.12-slim-bookworm 197 | stage: pages 198 | variables: 199 | GIT_DEPTH: 0 200 | only: 201 | refs: 202 | - main 203 | changes: 204 | - docs/**/* 205 | - mkdocs.yml 206 | dependencies: 207 | - poetry 208 | before_script: 209 | - apt-get update -y --quiet 210 | - apt-get install -y --quiet --no-install-recommends git 211 | - source .venv/bin/activate 212 | script: 213 | - mkdocs build 214 | - cp -vr site public 215 | # optionally, you can activate gzip support with the following line: 216 | - find public -type f -regex '.*\.\(htm\|html\|txt\|text\|js\|css\)$' -exec gzip -f -k {} \; 217 | 218 | artifacts: 219 | paths: 220 | - public 221 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.nvmrc: -------------------------------------------------------------------------------- 1 | 22.4 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.5.0 6 | hooks: 7 | - id: check-added-large-files 8 | args: ["--maxkb=5000"] 9 | - id: check-ast 10 | - id: fix-byte-order-marker 11 | - id: check-case-conflict 12 | - id: check-docstring-first 13 | - id: check-json 14 | - id: check-toml 15 | - id: check-yaml 16 | args: ["--unsafe"] 17 | - id: debug-statements 18 | - id: detect-private-key 19 | - id: end-of-file-fixer 20 | exclude: ^.+\.min\.(js|css)$ 21 | - id: mixed-line-ending 22 | args: ["--fix=lf"] 23 | - id: trailing-whitespace 24 | - repo: https://github.com/astral-sh/ruff-pre-commit 25 | rev: v0.5.1 26 | hooks: 27 | - id: ruff 28 | args: [--fix, --exit-non-zero-on-fix] 29 | - repo: https://github.com/psf/black 30 | rev: 24.4.2 31 | hooks: 32 | - id: black 33 | - repo: https://github.com/Riverside-Healthcare/djLint 34 | rev: v1.34.1 35 | hooks: 36 | - id: djlint-django 37 | - repo: https://github.com/commitizen-tools/commitizen 38 | rev: v3.14.1 39 | hooks: 40 | - id: commitizen 41 | stages: [commit-msg] 42 | - repo: https://github.com/pre-commit/mirrors-prettier 43 | rev: v4.0.0-alpha.8 44 | hooks: 45 | - id: prettier 46 | stages: [commit] 47 | exclude: > 48 | (?x)^( 49 | (.*)/static| 50 | (.*)/vendors| 51 | ^.+\.html$| 52 | ^.+\.mjml$| 53 | package-lock.json| 54 | (.*)/img/lottiefiles| 55 | ^.+\.min\.(js|css)$ 56 | )$ 57 | additional_dependencies: 58 | - prettier@3.3.3 59 | - repo: https://github.com/thibaudcolas/pre-commit-stylelint 60 | rev: v16.7.0 61 | hooks: 62 | - id: stylelint 63 | additional_dependencies: 64 | - "postcss-scss" 65 | - "stylelint@16.7.0" 66 | - "stylelint-config-standard-scss@13.1.0" 67 | exclude: > 68 | (?x)^( 69 | (.*)/static| 70 | (.*)/vendors| 71 | ^.+\.min\.(css)$ 72 | )$ 73 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore CHANGELOG 2 | CHANGELOG.md 3 | 4 | # Ignore all HTML/MJML files: 5 | *.html 6 | *.mjml 7 | # **/**/*.html 8 | # **/**/*.mjml 9 | 10 | # Ignore images 11 | **/assets/img/**/*.* 12 | 13 | # Ignore favicon webmanifest 14 | **/assets/ico/**/*.* 15 | 16 | # Ignore things in the vendors folder 17 | **/vendors/**/*.* 18 | 19 | # ignore lottiefiles 20 | **/img/lottiefiles/*.json 21 | 22 | # Ignore static assets build folder 23 | **/static/**/*.* 24 | 25 | # Ignore collectstatic folder 26 | **/staticfiles/**/*.* 27 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented here. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project attempts to adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/Dockerfile: -------------------------------------------------------------------------------- 1 | ################################################################################# 2 | # use node:22.4-bookworm as the base image for building the frontend 3 | ################################################################################# 4 | 5 | FROM node:22.4-bookworm AS frontend-builder 6 | 7 | # A wildcard is used to ensure both package.json AND package-lock.json are copied 8 | # where available (npm@5+) 9 | COPY package*.json .babelrc.js webpack.config.js ./ 10 | RUN npm ci --no-optional --no-audit --progress=false --network=host 11 | 12 | COPY ./{{cookiecutter.project_slug}}/assets ./{{cookiecutter.project_slug}}/assets 13 | RUN npm run build:prod 14 | 15 | ################################################################################# 16 | # use python:3.12-slim-bookworm as the base image for production and development 17 | ################################################################################# 18 | 19 | FROM python:3.12-slim-bookworm AS production 20 | 21 | # Add user that will be used in the container 22 | RUN groupadd wagtail && \ 23 | useradd --create-home --shell /bin/bash -g wagtail wagtail 24 | 25 | RUN mkdir -p /home/wagtail/app && chown wagtail:wagtail /home/wagtail/app 26 | 27 | # set work directory 28 | WORKDIR /home/wagtail/app 29 | 30 | # Port used by this container to serve HTTP. 31 | EXPOSE 8000 32 | 33 | # set environment variables 34 | # 1. Force Python stdout and stderr streams to be unbuffered. 35 | # 2. Set PORT variable that is used by Gunicorn. This should match "EXPOSE" 36 | # command. 37 | ENV PYTHONUNBUFFERED=1 \ 38 | PYTHONHASHSEED=random \ 39 | PYTHONPATH=/home/wagtail/app \ 40 | DJANGO_SETTINGS_MODULE={{cookiecutter.project_slug}}.settings.production \ 41 | ## Note: feel free to adjust WEB_CONCURRENCY based on the memory requirements of your processes 42 | ## ref: https://docs.gunicorn.org/en/stable/settings.html 43 | ## The suggested number of workers is (2*CPU)+1 44 | WEB_CONCURRENCY=3 \ 45 | NODE_MAJOR=22 46 | 47 | # Install system dependencies required by Wagtail, Django and the project 48 | RUN apt-get update --yes --quiet && apt-get install --yes --quiet --no-install-recommends \ 49 | build-essential \ 50 | ca-certificates gnupg \ 51 | curl \ 52 | gdal-bin libgdal-dev binutils libproj-dev \ 53 | git \ 54 | imagemagick \ 55 | libjpeg62-turbo-dev \ 56 | libmagic1 \ 57 | libpq-dev \ 58 | libwebp-dev \ 59 | zlib1g-dev \ 60 | && apt-get clean 61 | 62 | # Install node (Keep the version in sync with the node container above) 63 | RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_MAJOR}.x | bash - && \ 64 | apt-get install -y nodejs 65 | 66 | # Use user "wagtail" to run the build commands below and the server itself. 67 | USER wagtail 68 | 69 | # set up virtual environment & install python dependencies 70 | ARG DEVELOPMENT 71 | ARG POETRY_VERSION=1.8.3 72 | ENV VIRTUAL_ENV=/home/wagtail/venv \ 73 | DEVELOPMENT=${DEVELOPMENT} 74 | RUN python -m venv $VIRTUAL_ENV 75 | ENV PATH="$VIRTUAL_ENV/bin:$PATH" 76 | RUN pip install --upgrade pip 77 | RUN python -m pip install poetry==$POETRY_VERSION 78 | 79 | COPY --chown=wagtail ./pyproject.toml . 80 | COPY --chown=wagtail ./poetry.lock . 81 | RUN poetry install ${DEVELOPMENT:+--with dev,test,docs} --no-root 82 | 83 | # install mjml 84 | # NOTE: the version must match the one in package.json 85 | RUN npm install -D mjml@"^4.15.3" 86 | 87 | # Copy build artifacts from frontend-builder stage 88 | RUN mkdir -p /home/wagtail/app/{{cookiecutter.project_slug}}/static 89 | COPY --from=frontend-builder --chown=wagtail:wagtail /{{cookiecutter.project_slug}}/static /home/wagtail/app/{{cookiecutter.project_slug}}/static 90 | 91 | # Copy the source code of the project into the container 92 | COPY --chown=wagtail:wagtail . . 93 | 94 | # Run poetry install again to install the project (so that the `{{cookiecutter.project_slug}}` package is always importable) 95 | RUN poetry install 96 | 97 | # Run collectstatic. 98 | # This step is deferred, because it somehow messes up production settings 99 | # RUN python manage.py collectstatic --noinput --clear 100 | 101 | # Runtime command that executes when "docker run" is called 102 | CMD gunicorn {{cookiecutter.project_slug}}.wsgi:application 103 | 104 | ################################################################################# 105 | # The next steps won't be run in production 106 | ################################################################################# 107 | 108 | FROM production AS dev 109 | 110 | # Swap user, so the following tasks can be run as root 111 | USER root 112 | 113 | # Install `psql`, useful for `manage.py dbshell` 114 | RUN apt-get install -y postgresql-client 115 | 116 | # Restore user 117 | USER wagtail 118 | 119 | # Pull in the node modules for the frontend 120 | COPY --chown=wagtail:wagtail --from=frontend-builder ./node_modules ./node_modules 121 | 122 | # do nothing - exec commands elsewhere 123 | CMD tail -f /dev/null 124 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/LICENSE: -------------------------------------------------------------------------------- 1 | # TODO: add license 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn {{cookiecutter.project_slug}}.wsgi:application 2 | 3 | release: python manage.py migrate --no-input 4 | 5 | # worker: python manage.py rqworker default 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/README.md: -------------------------------------------------------------------------------- 1 | # {{ cookiecutter.project_name }} 2 | 3 | > {{ cookiecutter.description }} 4 | 5 | [![forthebadge](https://forthebadge.com/images/badges/made-with-python.svg)](https://forthebadge.com) 6 | 7 | [![python3](https://img.shields.io/badge/python-3.12-brightgreen.svg)](https://python.org/) 8 | [![Node v22](https://img.shields.io/badge/Node-v22-teal.svg)](https://nodejs.org/en/blog/release/v22.0.0) 9 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 10 | [![code style: prettier](https://img.shields.io/badge/code%20style-prettier-ff69b4.svg)](https://prettier.io/) 11 | 12 | [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) 13 | [![Conventional Changelog](https://img.shields.io/badge/changelog-conventional-brightgreen.svg)](https://github.com/conventional-changelog) 14 | [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org) 15 | 16 | 17 | 18 | 19 | - [Introduction](#introduction) 20 | - [Development](#development) 21 | - [First things first](#first-things-first) 22 | - [Getting Started](#getting-started) 23 | - [Commits, Releases and Changelogs](#commits-releases-and-changelogs) 24 | - [Tips](#tips) 25 | - [Project Technical Documentation](#project-technical-documentation) 26 | - [Credits](#credits) 27 | 28 | 29 | 30 | ## Introduction 31 | 32 | This is a [Python](https://www.python.org/) project built using [Wagtail](https://wagtail.org/) – a powerful [Django](https://www.djangoproject.com/) Content Management System. 33 | 34 | - As with most web projects, the frontend dependencies, tasks, etc. are managed using [Node.js](https://nodejs.org/). This project uses [Webpack](https://webpack.js.org/) to bundle frontend assets. 35 | - Tests via [pytest](https://pytest.org/) 36 | - Linting and formatting: 37 | - _python_: [Black](https://black.readthedocs.io/) and [ruff](https://github.com/astral-sh/ruff) 38 | - _frontend_: [ESLint](https://eslint.org/), [Stylelint](https://stylelint.io/), [prettier](https://prettier.io/) and [djLint](https://www.djlint.com/). 39 | - Task execution and automation using [`invoke`](http://www.pyinvoke.org/). 40 | - [Continuous integration (CI)](https://www.atlassian.com/continuous-delivery/continuous-integration) via [Github Actions](https://github.com/features/actions) / [Gitlab CI/CD](https://docs.gitlab.com/ee/ci/). 41 | 42 | ## Development 43 | 44 | ### First things first 45 | 46 | Start by ensuring that you have Docker and Docker Compose: 47 | 48 | ```sh 49 | # check that you have docker on your machine 50 | docker -v 51 | 52 | # check that you have docker-compose on your machine 53 | docker-compose -v 54 | ``` 55 | 56 | For the best developer experience, you need to have [Python 3.12](https://www.python.org/) and [Poetry](https://python-poetry.org/) installed on your machine. If, for some reason, you have a different python version, you can use [pyenv](https://github.com/pyenv/pyenv) to install multiple python versions on your machine. Once you have Python 3.12 installed, create a [**virtual environment**](https://realpython.com/python-virtual-environments-a-primer/) and install dependencies via `poetry install --with dev,test,docs`. 57 | 58 | ### Getting Started 59 | 60 | Here, we assume that you have `git` on your machine, and that you have created a Python 3.12 virtual environment and installed the development dependencies. 61 | 62 | Now, upon cloning this repository (or forking + cloning your fork), navigate to the cloned project directory. 63 | 64 | Skip the next step (involving `.env`) if you have just created a new project using the [`cookiecutter-wagtail-vix`](https://github.com/engineervix/cookiecutter-wagtail-vix) project template. 65 | 66 | Then create the required `.env` file: 67 | 68 | ```sh 69 | cp -v .env.sample .env 70 | ``` 71 | 72 | At this point, you might wanna edit `.env` by replacing `CHANGEME!!` with appropriate values, as indicated in the comments above such an environment variable. You can leave the other values as they are. 73 | 74 | Now, build the images and spin up the containers: 75 | 76 | ```sh 77 | inv up --build 78 | ``` 79 | 80 | This is basically the same as running `docker-compose up -d --build`, but is obviously much shorter 😎. The above is made possible by [Invoke](https://www.pyinvoke.org/), which is [used extensively on this project to automate some tasks](#tips). Also note that `inv` is short for `invoke` — the two can be used interchangeably. 81 | 82 | Running the above command may take a while, you might wanna grab a cup of tea ☕. 83 | 84 | > **Note** 85 | > 86 | > every time you want to spin up the containers, you can just run `inv up` without specifying the `--build` argument. Only add the `--build` argument if you wanna rebuild the images. 87 | 88 | If everything goes well, you should be able to get into the `web` container and access the shell. 89 | 90 | ```sh 91 | inv exec web bash 92 | ``` 93 | 94 | Once you're in the container, 95 | 96 | - apply database migrations via `./manage.py migrate`, 97 | - [create a cache table](https://docs.djangoproject.com/en/5.0/topics/cache/#creating-the-cache-table) via `./manage.py createcachetable` 98 | - create a `superuser` via `./manage.py createsuperuser`, 99 | - run the following to simultaneously launch the [django development server](https://docs.djangoproject.com/en/5.0/ref/django-admin/#django-admin-runserver) and the [webpack dev server](https://webpack.js.org/configuration/dev-server/): 100 | 101 | ```sh 102 | inv start 103 | ``` 104 | 105 | You can access the dev server at . This project uses [MailDev](https://github.com/maildev/maildev) for viewing and testing emails generated during development. The `MailDev` server is accessible at . 106 | 107 | ### Commits, Releases and Changelogs 108 | 109 | This project follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification for structured and [semantic](https://semver.org/spec/v2.0.0.html) commit messages. It also utilizes a [conventional changelog](https://github.com/conventional-changelog/conventional-changelog#getting-started) to keep track of changes and releases in a standardized way. 110 | 111 | Creating a release is as simple as running 112 | 113 | ```bash 114 | inv bump main 115 | ``` 116 | 117 | Assuming you are working with the `main` branch. 118 | 119 | If it's your first release: 120 | 121 | ```bash 122 | inv bump main --first 123 | ``` 124 | 125 | This will 126 | 127 | - create a `v0.0.0` and a `v0.1.0` tag 128 | - update the changelog accordingly 129 | - push the changes to your origin and create a release, complete with release notes. 130 | 131 | For the first release, you can also supply the `--major` argument and this will create a `v1.0.0` tag instead of `v0.1.0` 132 | 133 | ### Tips 134 | 135 | - Run `invoke -l` to see all available [Invoke](https://www.pyinvoke.org/) tasks. These are defined in the [tasks.py](tasks.py) file. 136 | - You'll want to setup [pre-commit](https://pre-commit.com/) by running `pre-commit install` followed by `pre-commit install --hook-type commit-msg`. Optionally run `pre-commit run --all-files` to make sure your pre-commit setup is okay. 137 | - You'll probably also want to install Node.js 22 on your machine, together with the dependencies. We recommend using [fnm](https://github.com/Schniz/fnm) or [volta](https://volta.sh/) to simplify managing Node.js versions on your machine. 138 | 139 | ## Project Technical Documentation 140 | 141 | The project's documentation is powered by [mkdocs](https://www.mkdocs.org/), and lives in the [`docs`](./docs/) directory. 142 | 143 | You can view it by running the following in the `web` container: 144 | 145 | ```bash 146 | mkdocs serve 147 | ``` 148 | 149 | The documentation will be available at: 150 | 151 | ## Credits 152 | 153 | This project was created with [Cookiecutter](https://github.com/audreyr/cookiecutter) and the [`engineervix/cookiecutter-wagtail-vix`](https://github.com/engineervix/cookiecutter-wagtail-vix) project template. 154 | 155 | --- 156 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "formation": { 3 | "web": { 4 | "quantity": 1 5 | }, 6 | "worker": { 7 | "quantity": 1 8 | } 9 | }, 10 | "cron": [ 11 | { 12 | "command": "python manage.py clearsessions", 13 | "schedule": "@daily" 14 | } 15 | ], 16 | "scripts": { 17 | "dokku": { 18 | "predeploy": "./bin/post_compile" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/bin/post_compile: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # the script should exit whenever it encounters an error 4 | set -o errexit 5 | # exit execution if one of the commands in the pipe fails. 6 | set -o pipefail 7 | # exit when the script tries to use undeclared variables. 8 | set -o nounset 9 | 10 | python manage.py collectstatic --noinput --clear 11 | python manage.py check --deploy 12 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | web: 3 | build: 4 | context: . 5 | args: 6 | DEVELOPMENT: 1 7 | target: dev 8 | command: tail -f /dev/null 9 | ports: 10 | - 8000:8000 11 | - 8001:8001 # mkdocs 12 | env_file: 13 | - .env 14 | environment: 15 | - DJANGO_SETTINGS_MODULE={{cookiecutter.project_slug}}.settings.dev 16 | - NODE_ENV=development 17 | volumes: 18 | - ./:/home/wagtail/app/ 19 | depends_on: 20 | - db 21 | # - redis 22 | - maildev 23 | 24 | db: 25 | build: 26 | context: ./docker/db 27 | dockerfile: Dockerfile 28 | expose: 29 | - 5432 30 | environment: 31 | - POSTGRES_USER=wagtail_dev_user 32 | - POSTGRES_PASSWORD=wagtail_dev_password 33 | volumes: 34 | - postgres_data:/var/lib/postgresql/data/ 35 | 36 | # redis: 37 | # image: redis:7-alpine 38 | 39 | # worker: 40 | # build: 41 | # context: . 42 | # args: 43 | # DEVELOPMENT: 1 44 | # target: dev 45 | # command: python manage.py rqworker default 46 | # env_file: 47 | # - .env 48 | # volumes: 49 | # - ./:/home/wagtail/app/ 50 | # environment: 51 | # - DJANGO_SETTINGS_MODULE={{cookiecutter.project_slug}}.settings.dev 52 | # - NODE_ENV=development 53 | # - RQ_QUEUE=redis://redis:6379/0 54 | # depends_on: 55 | # - web 56 | # - redis 57 | # - maildev 58 | 59 | maildev: 60 | image: maildev/maildev 61 | ports: 62 | - "1080:1080" 63 | 64 | volumes: 65 | postgres_data: 66 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/docker/Procfile: -------------------------------------------------------------------------------- 1 | web: inv dev 2 | frontend: npm run dev:reload 3 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/docker/README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | This directory contains [Docker](https://www.docker.com/) configuration files for **development**. 4 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/docker/db/Dockerfile: -------------------------------------------------------------------------------- 1 | # pull official base image 2 | FROM postgis/postgis:15-3.4 3 | 4 | # run create.sql on init 5 | ADD create.sql /docker-entrypoint-initdb.d/ 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/docker/db/create.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE wagtail_dev_db; 2 | CREATE DATABASE wagtail_test_db; 3 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/docker/docker-compose-frontend.yml: -------------------------------------------------------------------------------- 1 | services: 2 | web: 3 | ports: 4 | - 3000:3000 # browser-sync 5 | - 3001:3001 # browser-sync UI 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/docs/architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | The architecture of _{{ cookiecutter.project_name }}_ consists of several key components that work together to deliver the application's functionality. Understanding the architecture will help you navigate the codebase and make informed decisions during development and maintenance. 4 | 5 | ![Architecture Diagram](images/architecture.png){ data-description="{{ cookiecutter.project_name }} Architecture Diagram" } 6 | 7 | !!! note 8 | 9 | The project runs on [Docker](https://www.docker.com/) 10 | 11 | The organization and structure of the project's source code is discussed in the [Project Structure](./project-structure.md) section. 12 | 13 | --- 14 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | Here, we'll highlight some important custom configurations for the project as well as third party integrations / apps. 4 | 5 | ??? tip 6 | 7 | Many of the things discussed here are configured as part of the project's [Django Settings](https://docs.djangoproject.com/en/5.0/topics/settings/). The [settings files](./project-structure.md#settings) themselves are well documented, with links to either the official Django docs or third-party package docs where applicable. 8 | 9 | ## Media storage 10 | 11 | _{{ cookiecutter.project_name }}_ uses [django-storages](https://django-storages.readthedocs.io/en/latest/index.html) in production, and is configured to use an [S3-compatible](https://www.techtarget.com/searchstorage/tip/How-to-use-S3-compatible-storage) object-storage such as [Backblaze B2](https://www.backblaze.com/b2/docs/s3_compatible_api.html). 12 | 13 | ???+ note 14 | 15 | We use this kind of setup for the following reasons: 16 | 17 | 1. **Data Persistence**: [Containers](https://www.docker.com/resources/what-container/) are ephemeral by nature, and any data stored within them can be lost when the container is stopped or restarted. Storing media files locally within a Docker container makes it challenging to persist those files. Django Storages allows you to store media files in a more durable and persistent location, ensuring that your files are retained even if containers are replaced or restarted. 18 | 2. **Maintenance**: Managing media files directly within containers can complicate maintenance and updates. Separating media storage from the containerized application simplifies maintenance tasks. 19 | 3. **Efficient Backups**: Storing media files in a cloud-based storage service often includes built-in backup and redundancy features. This ensures that your media files are safe from data loss due to hardware failures or other unforeseen issues. 20 | 21 | ??? tip 22 | 23 | [Backblaze B2](https://www.backblaze.com/b2/docs/s3_compatible_api.html) is generally cheaper and esier to set up than [AWS S3](https://aws.amazon.com/s3/)[^1]. To use it as a _django-storages_ backend, [read the setup instructions here](https://django-storages.readthedocs.io/en/stable/backends/backblaze-B2.html). 24 | 25 | **Charges** 26 | 27 | - For **storage**, they charge $0.005/GB/Month, the first 10GB of storage is free. 28 | - For **downloads**, they charge $0.01/GB. The first 1GB of data downloaded each day is free. 29 | - There are also what they call **transaction** charges – read more about them at . Class “B” transactions are $0.004 per 10,000 with 2,500 free per day. Class “C” transactions are $0.004 per 1,000 with 2,500 free per day. 30 | 31 | ## Database 32 | 33 | This project uses [Postgres](https://www.postgresql.org/) throughout its lifecycle, that is, from development to production. Postgres has earned a strong reputation for its proven architecture, reliability, data integrity, robust feature set, extensibility, and the dedication of the open-source community behind the software to consistently deliver performant and innovative solutions. 34 | 35 | Using Postgres and Django together offers many benefits: 36 | 37 | - Django provides a number of data types that will only work with Postgres. 38 | - Django has `django.contrib.postgres` to make database operations on Postgres. 39 | - Applications that store geographical data need to use Postgres (with the [PostGIS](https://postgis.net/) extension), as [GeoDjango](https://docs.djangoproject.com/en/5.0/ref/contrib/gis/) is only _fully_ compatible with Postgres. 40 | - Postgres has the richest set of features that are supported by Django. 41 | 42 | ???+ info 43 | 44 | Here are some of the PostgreSQL-specific features supported by Django: 45 | 46 | - [PostgreSQL-specific aggregation functions](https://docs.djangoproject.com/en/5.0/ref/contrib/postgres/aggregates/) 47 | - [PostgreSQL-specific database constraints](https://docs.djangoproject.com/en/5.0/ref/contrib/postgres/constraints/) 48 | - [PostgreSQL-specific form fields and widgets](https://docs.djangoproject.com/en/5.0/ref/contrib/postgres/forms/) 49 | - [PostgreSQL-specific database functions](https://docs.djangoproject.com/en/5.0/ref/contrib/postgres/functions/) 50 | - [PostgreSQL-specific model indexes](https://docs.djangoproject.com/en/5.0/ref/contrib/postgres/indexes/) 51 | - [PostgreSQL-specific lookups](https://docs.djangoproject.com/en/5.0/ref/contrib/postgres/lookups/) 52 | - [Database migration operations](https://docs.djangoproject.com/en/5.0/ref/contrib/postgres/operations/) 53 | - [Full-text search](https://docs.djangoproject.com/en/5.0/ref/contrib/postgres/search/) 54 | - [Validators](https://docs.djangoproject.com/en/5.0/ref/contrib/postgres/validators/) 55 | 56 | ## Sending emails 57 | 58 | Sending emails is an important part of _{{ cookiecutter.project_name }}_, and we want to ensure that we are using a reliable system so that there are no issues with email delivery. 59 | 60 | As indicated in the project's README, _{{ cookiecutter.project_name }}_ uses [maildev](https://github.com/maildev/maildev) during development to test emails. In production, we recommend using [django-anymail](https://github.com/anymail/django-anymail) and is configuring it with a suitable transactional email service provider, such as [Brevo](https://www.brevo.com/pricing/), [Mailjet](https://www.mailjet.com/pricing/), [Mailgun](https://www.mailgun.com/pricing/), [Sendgrid](https://sendgrid.com/en-us/pricing), [Postmark](https://postmarkapp.com/pricing) and so on. 61 | 62 | With so many options for this, the choice is really dependent on the client's preferences and budget. [Mailjet](https://www.mailjet.com/pricing/) is relatively easy to setup, plus it has a [generous free tier](https://www.mailjet.com/pricing/) – you can send 200 emails per day (6,000 emails/month) for free. If you need to send more emails, then the next plan costs $15/month (15,000 emails/month, no daily limit). [Brevo](https://www.brevo.com/pricing/) has an even more generous free tier at 300 emails per day! The next plan is also $15/month, but allows for up to 20,000 emails/month. 63 | 64 | Whichever provider you settle for, check the [django-anymail](https://github.com/anymail/django-anymail) docs on how to configure the specified provider. You'll obviously have to update _django-anymail_'s `extras` parameter in `pyproject.toml`, and [update the project dependencies accordingly](https://realpython.com/dependency-management-python-poetry/#handle-poetrylock). 65 | 66 | You can of course just use [Django's built-in SMTP backend](https://docs.djangoproject.com/en/5.0/topics/email/#smtp-backend). 67 | 68 | ## Worker 69 | 70 | This project is configured to work with [django-rq](https://github.com/rq/django-rq) to allow fo scheduling background tasks outside the request-response cycle. 71 | 72 | During development, there's a `worker` container that starts automatically when you start the containers with `inv up`. You can check its logs via `inv logs worker`, and if you want to follow log output, add the `-f` argument: `inv logs worker -f`. 73 | 74 | In production, you'll see from the `Procfile` that there's a command specified for the `worker` process, whose formation is defined in `app.json`. 75 | 76 | ## Periodic tasks 77 | 78 | While periodic tasks can be executed using [RQ](https://python-rq.org/), we recommend using standard cron schedulers as this is simpler and more cost effective. If you're deploying to Heroku, use their [Heroku Scheduler](https://devcenter.heroku.com/articles/scheduler), which is a free addon. If you're deploying to Dokku, use the [Dokku Managed Cron](https://dokku.com/docs/processes/scheduled-cron-tasks/?h=cron#dokku-managed-cron), whose configuration is done in the project's `app.json` file. The configuration, as of the time of writing these docs, looks like this 79 | 80 | ```json 81 | "cron": [ 82 | { 83 | "command": "python manage.py clearsessions", //(1)! 84 | "schedule": "@daily" 85 | } 86 | ], 87 | ``` 88 | 89 | 1. This cleans out expired sessions. See [the docs](https://docs.djangoproject.com/en/5.0/ref/django-admin/#django-admin-clearsessions). 90 | 91 | [^1]: However, if you want more fine-grained control and greater flexibility, you probably wanna use [AWS S3](https://aws.amazon.com/s3/). 92 | 93 | --- 94 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/docs/dev-setup.md: -------------------------------------------------------------------------------- 1 | # Development Setup 2 | 3 | The development setup is outlined in the project's **README**. In keeping things [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself), we will not reproduce the README contents here. 4 | 5 | It's important to note that, as a minimum you'll need to have both [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/) installed on your machine. 6 | 7 | If you are reading these docs, then we can assume that you have access to the project's git repository, and have already cloned it (or forked and cloned your fork). Please see the **README** for details of how to setup the project on your machine. 8 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/docs/frontend.md: -------------------------------------------------------------------------------- 1 | # Frontend tooling 2 | 3 | ## What's required 4 | 5 | To install Node.js on the host machine you could use tools like [fnm](https://github.com/Schniz/fnm), [`volta`](https://volta.sh/) and so on. Once you have Node.js 22 installed simply run `npm install` in the project root to install the dependencies. 6 | 7 | ## Starting afresh 8 | 9 | The _{{ cookiecutter.project_name }}_ frontend tooling is versioned via `package.json`, and the `package-lock.json` lockfile pins all of the project’s direct and transitive dependencies. If you wish to start the project with up to date dependencies without doing manual upgrades, you can discard the lockfile and re-create it: 10 | 11 | ```sh 12 | rm -rf node_modules 13 | rm package-lock.json 14 | npm install 15 | ``` 16 | 17 | Remember to then commit the updated `package-lock.json`. 18 | 19 | ## What's included 20 | 21 | - [Sass](http://sass-lang.com/) CSS with [auto-prefixing](https://github.com/postcss/autoprefixer). 22 | - [webpack-dev-server](https://github.com/webpack/webpack-dev-server) for live reloading. 23 | - [Webpack](https://webpack.js.org/) for module bundling. 24 | - With `babel-loader` to process JavaScript. 25 | - With `css-loader`, `postcss-loader`, and `sass-loader` to process stylesheets. 26 | - Consideration for images, currently copying the directory only - to avoid slowdowns and non-essential dependencies. It is recommended to use SVG for UI vectors and pre-optimised UI photograph assets. 27 | - [Build commands](#build-scripts) for generating testable or deployable assets only 28 | - CSS linting with `stylelint` 29 | - JS linting with `eslint` 30 | - Code formatting with [Prettier](https://prettier.io/) 31 | 32 | ## Development 33 | 34 | - The development environment is executed via: 35 | 36 | ```bash 37 | npm run dev:reload # (1)! 38 | ``` 39 | 40 | 1. Note that this is automatically executed when you run `inv start` in the `web` container. For details, see `docker/Procfile`. 41 | 42 | - Source files for developing your project are in `assets` and the distribution folder for the compiled assets is `static`. Don't make direct changes to the `static` directory as they will be overwritten. 43 | 44 | ## Deployment 45 | 46 | ### Build scripts 47 | 48 | To only build assets for either development or production you can use 49 | 50 | - `npm run build` To build development assets 51 | - `npm run build:prod` To build assets with minification and vendor prefixes 52 | 53 | ## Further details of key packages included 54 | 55 | ### Dependencies 56 | 57 | - [bootstrap 5](https://getbootstrap.com/docs/5.3/getting-started/introduction/) – Powerful, extensible, and feature-packed frontend toolkit. 58 | 59 | ### Development Dependencies 60 | 61 | - **autoprefixer** - adds vendor prefixes as necessary for the browsers defined in `browserslist` in the npm config https://www.npmjs.com/package/autoprefixer 62 | - **babel-loader** - transpile JavaScript files using Babel and webpack - https://github.com/babel/babel-loader 63 | - **copy-webpack-plugin** - Used to sync images from `assets` to `static` 64 | - **css-loader** – add support for Webpack to load stylesheets. 65 | - **cssnano** – minify CSS with safe optimisations - https://cssnano.co/. 66 | - **eslint** - lint your javascript https://www.npmjs.com/package/eslint 67 | - **eslint-config-prettier** - Turns off all rules that are unnecessary or might conflict with [Prettier](https://github.com/prettier/prettier) - https://github.com/prettier/eslint-config-prettier 68 | - **eslint-plugin-prettier** - ESLint plugin for Prettier formatting - https://github.com/prettier/eslint-plugin-prettier 69 | - **mini-css-extract-plugin** - extract CSS generated by Webpack into separate files. 70 | - **mjml** - a framework that simplifies responsive-email - https://github.com/mjmlio/mjml 71 | - **postcss-loader** - integrate PostCSS preprocessing into Webpack’s styles loading. 72 | - **postcss-custom-properties** - polyfill for CSS custom properties - https://www.npmjs.com/package/postcss-custom-properties 73 | - **stylelint** - Linting for styles - https://stylelint.io 74 | - **stylelint-config-standard-scss** - The standard shareable SCSS config for Stylelint - https://www.npmjs.com/package/stylelint-config-standard-scss 75 | - **sass-loader** - integrate Sass preprocessing into Webpack’s styles loading. 76 | - **webpack** - Bundler for js files (can do much more too) - https://www.npmjs.com/package/webpack https://webpack.js.org/concepts/ 77 | - **webpack-cli** - Provides a set of tools to improve the setup of custom webpack configuration - https://www.npmjs.com/package/webpack-cli 78 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/docs/images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engineervix/cookiecutter-wagtail-vix/66d5ca64237bd487092b2a65986a9254a693a008/{{cookiecutter.project_slug}}/docs/images/architecture.png -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/docs/index.md: -------------------------------------------------------------------------------- 1 | # Docs 2 | 3 | [![forthebadge](https://forthebadge.com/images/badges/made-with-markdown.svg)](https://www.markdownguide.org/) 4 | 5 | Welcome to the technical documentation for the [{{ cookiecutter.project_name }}]({{ cookiecutter.domain_name }}) [Wagtail](https://wagtail.org/) project. 6 | 7 | This documentation is written in [Markdown](https://www.markdownguide.org/) and the documentation site is powered by [mkdocs.org](https://www.mkdocs.org), using the [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) theme. 8 | 9 | --- 10 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/docs/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | [![forthebadge](https://forthebadge.com/images/badges/made-with-python.svg)](https://www.python.org) 4 | 5 | _{{ cookiecutter.project_name }}_ is a [Python](https://www.python.org/) web application built using [Django](https://www.djangoproject.com/) as its foundational high-level web framework, augmented by [Wagtail](https://wagtail.org/) – a content management system (CMS) that leverages the capabilities of Django. Django encourages rapid development and clean, pragmatic design, while Wagtail builds on top of Django to enhance content management capabilities. Wagtail provides a user-friendly and flexible solution for creating and managing dynamic web content. Together, they form a powerful stack, combining Django's robustness with Wagtail's specialized content management features, making it an ideal choice for projects that demand a solid web framework with advanced content management capabilities. 6 | 7 | ## Project Overview 8 | 9 | TODO: add content ... 10 | 11 | ## Read the docs 12 | 13 | By following this documentation, you will gain a deeper understanding of the project's technical intricacies and be able to effectively contribute to its development, maintenance, and overall success. 14 | 15 | Next, we will look at the project's architecture. Let's get started! 16 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/docs/maintenance.md: -------------------------------------------------------------------------------- 1 | # Maintenance 2 | 3 | ## Backups 4 | 5 | Ensure that you run regular backups of the database. If you're using Dokku, you will see from the [Deployment](./deployment.md) section that we configure automatic periodic backups to a private Backblaze bucket. If, for instance, something went wrong with your server and you had to do a redeployment, you can restore your database from a backup as follows (assuming your postgres database is called `postgres-{{cookiecutter.project_slug|replace('_', '-')}}`) 6 | 7 | ```bash 8 | # NOTE: your database backup will probably be gzipped, you must extract it 9 | dokku postgres:import postgres-{{cookiecutter.project_slug|replace('_', '-')}} < /path/to/your/databasebackup 10 | ``` 11 | 12 | ## Dependencies 13 | 14 | _{{ cookiecutter.project_name }}_ has a configuration for [renovate](https://renovatebot.com/) – a dependency update tool that helps keep the project's dependencies up-to-date. If you use GitHub, you should get Pull Requests from the Renovate Bot whenever there are package updates. Security updates will automatically be merged. However, minor updates and major updates may potentially require manual checking and testing before incorporating them into the project, to avoid possibly breaking project functionality. If you are using Gitlab or something else, you'll have to figure out how to setup renovate (or similar tools) there. 15 | 16 | ## Scaling 17 | 18 | Scaling your project is essential as it grows to accommodate increased traffic and user demands. If you have deployed to Dokku, then you'll be happy to know that Dokku provides a flexible and straightforward way to scale your application horizontally and manage multiple instances. 19 | 20 | In `app.json`, we've set the default scaling to 1 `web` process and 1 `worker` process. You can increase these if needed. See for more details. 21 | 22 | ## Logging and Monitoring 23 | 24 | _{{ cookiecutter.project_name }}_ is configured to use [Sentry](https://sentry.io/) for application performance monitoring & error tracking. If something goes wrong, it should be captured by Sentry. 25 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/docs/references.md: -------------------------------------------------------------------------------- 1 | # References 2 | 3 | This section contains a list of resources, tools, and references that can be valuable for understanding and working with your project. 4 | 5 | ## Official Wagtail Documentation 6 | 7 | - [Wagtail Official Documentation](https://docs.wagtail.org/): The official documentation for Wagtail CMS. It covers everything from getting started to advanced topics, making it an invaluable resource for Wagtail development. 8 | 9 | ## Official Django Documentation 10 | 11 | - [Django Official Documentation](https://docs.djangoproject.com/): The official documentation for the Django web framework. It covers everything from getting started to advanced topics, making it an essential resource for Django development. 12 | 13 | ## Dokku Documentation 14 | 15 | - [Dokku Official Documentation](https://dokku.com/): The official documentation for Dokku, a platform you might want to use for deployment. It provides in-depth information on setting up and managing Dokku for your project. 16 | 17 | ## Dependency Management 18 | 19 | - [Renovate](https://docs.renovatebot.com/): The tool used to automate dependency updates in your project. Learn more about its features and how to configure it. 20 | 21 | ## MkDocs 22 | 23 | - [MkDocs Documentation](https://www.mkdocs.org/): We're using MkDocs to create and maintain your project's documentation, this is the official documentation for MkDocs. 24 | 25 | ## Database and Spatial Data (PostGIS) 26 | 27 | - [PostgreSQL Documentation](https://www.postgresql.org/docs/): We're using PostgreSQL as the database system, the official documentation is a valuable resource for managing your database. 28 | 29 | - [PostGIS Documentation](https://postgis.net/docs/): For working with geospatial and spatial data, PostGIS is an extension for PostgreSQL. Its documentation provides guidance on managing spatial data within your database. 30 | 31 | ## Caching 32 | 33 | - [Redis Documentation](https://redis.io/documentation): We're using Redis for caching, the official documentation covers its features and usage. 34 | 35 | ## Automated Testing 36 | 37 | - [pytest Documentation](https://docs.pytest.org/): Contains detailed guidance on writing and running tests. 38 | 39 | ## Security 40 | 41 | - [OWASP Top Ten Project](https://owasp.org/www-project-top-ten/): The Open Web Application Security Project's top ten list of web application security risks. Learn about common security vulnerabilities and how to mitigate them. 42 | 43 | ## Error Tracking 44 | 45 | - [Sentry Documentation](https://docs.sentry.io/): For error tracking and monitoring, Sentry's documentation explains how to set up and use their platform effectively. 46 | 47 | These references are intended to assist you in various aspects of your project, from development and deployment to maintenance and security. Use them as needed to enhance your understanding and proficiency with the technologies and tools involved. 48 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/docs/testing.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | This project utilizes the [pytest](https://pytest.org/) as the testing framework. 4 | 5 | Tests are organized into test suites to group related test cases together. Each Django app typically has its own test suite. You can find test suites in the `tests` directory of each app. 6 | 7 | Within each test suite, individual test cases are written to verify specific functionality or behavior of the code. These test cases are located in Python files within the test suite directory. 8 | 9 | To run the tests for the project, run either 10 | 11 | ```bash 12 | pytest 13 | ``` 14 | 15 | or 16 | 17 | ```bash 18 | invoke test 19 | ``` 20 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | if os.getenv("WEB_CONCURRENCY"): # Feel free to change this condition 10 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{cookiecutter.project_slug}}.settings.production") 11 | else: 12 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{cookiecutter.project_slug}}.settings.dev") 13 | 14 | try: 15 | from django.core.management import execute_from_command_line 16 | except ImportError as exc: 17 | raise ImportError( 18 | "Couldn't import Django. Are you sure it's installed and " 19 | "available on your PYTHONPATH environment variable? Did you " 20 | "forget to activate a virtual environment?" 21 | ) from exc 22 | execute_from_command_line(sys.argv) 23 | 24 | 25 | if __name__ == "__main__": 26 | main() 27 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/mkdocs.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://squidfunk.github.io/mkdocs-material/schema.json 2 | 3 | site_name: {{ cookiecutter.project_name }} 4 | dev_addr: 0.0.0.0:8001 5 | 6 | theme: 7 | name: material 8 | palette: 9 | # Palette toggle for light mode 10 | - media: "(prefers-color-scheme: light)" 11 | scheme: default 12 | toggle: 13 | icon: material/weather-sunny 14 | name: Switch to dark mode 15 | 16 | # Palette toggle for dark mode 17 | - media: "(prefers-color-scheme: dark)" 18 | scheme: slate 19 | toggle: 20 | icon: material/weather-night 21 | name: Switch to light mode 22 | features: 23 | - content.code.annotate 24 | - content.code.copy 25 | - navigation.footer 26 | - navigation.top 27 | - navigation.indexes 28 | - toc.follow 29 | 30 | repo_url: https://github.com/{{ cookiecutter.github_username }}/{{ cookiecutter.project_slug|replace('_', '-') }}.git 31 | edit_uri: edit/main/docs/ 32 | 33 | plugins: 34 | - search 35 | - glightbox 36 | - git-revision-date-localized: 37 | fallback_to_build_date: true 38 | enable_creation_date: true 39 | type: datetime 40 | 41 | markdown_extensions: 42 | - abbr 43 | - admonition 44 | - attr_list 45 | - def_list 46 | - codehilite 47 | - footnotes 48 | - md_in_html 49 | - sane_lists 50 | - toc: 51 | permalink: "¶" 52 | - pymdownx.betterem 53 | - pymdownx.caret 54 | - pymdownx.mark 55 | - pymdownx.tilde 56 | - pymdownx.critic 57 | - pymdownx.details 58 | - pymdownx.highlight: 59 | anchor_linenums: true 60 | line_spans: __span 61 | pygments_lang_class: true 62 | - pymdownx.inlinehilite 63 | - pymdownx.magiclink 64 | - pymdownx.emoji: 65 | emoji_index: !!python/name:material.extensions.emoji.twemoji 66 | emoji_generator: !!python/name:material.extensions.emoji.to_svg 67 | - pymdownx.keys 68 | - pymdownx.smartsymbols 69 | - pymdownx.snippets 70 | - pymdownx.tabbed: 71 | alternate_style: true 72 | - pymdownx.tasklist: 73 | custom_checkbox: true 74 | - pymdownx.superfences: 75 | custom_fences: 76 | - name: mermaid 77 | class: mermaid 78 | format: !!python/name:pymdownx.superfences.fence_code_format 79 | 80 | nav: 81 | - "Home": "index.md" 82 | - "Introduction": "introduction.md" 83 | - "Architecture": "architecture.md" 84 | - "Development Setup": "dev-setup.md" 85 | - "Project Structure": "project-structure.md" 86 | - "Configuration": "configuration.md" 87 | - "Frontend Tooling": "frontend.md" 88 | - "Testing": "testing.md" 89 | - "Deployment": "deployment.md" 90 | - "Maintenance": "maintenance.md" 91 | - "References": "references.md" 92 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "{{cookiecutter.project_slug}}", 3 | "version": "0.0.0", 4 | "description": "{{cookiecutter.description}}", 5 | "author": "{{ cookiecutter.author_name }}", 6 | "license": "UNLICENSED", 7 | "private": true, 8 | "scripts": { 9 | "build": "webpack --mode development --progress", 10 | "build:prod": "webpack --mode production", 11 | "commit": "git-cz", 12 | "css-fix": "npx stylelint {{cookiecutter.project_slug}}/static/css/ --fix", 13 | "dev": "webpack --mode development --progress --watch", 14 | "dev:reload": "webpack serve", 15 | "lint:format": "prettier --check '**/?(.)*.{md,css,scss,js,json,yaml,yml}'", 16 | "lint:js": "eslint --ext \".js\" --ignore-path .gitignore {{cookiecutter.project_slug}}/assets/js/", 17 | "lint:style": "stylelint \"{{cookiecutter.project_slug}}/assets/scss/**/*.{scss,css}\" --ignore-path .gitignore", 18 | "lint": "npm run lint:js && npm run lint:style && npm run lint:format", 19 | "release": "standard-version" 20 | }, 21 | "dependencies": { 22 | "bootstrap": "^5.3.3" 23 | }, 24 | "devDependencies": { 25 | "@babel/core": "^7.24.9", 26 | "@babel/eslint-parser": "^7.24.8", 27 | "@babel/preset-env": "^7.24.8", 28 | "autoprefixer": "^10.4.19", 29 | "babel-loader": "^9.1.3", 30 | "bourbon": "^7.3.0", 31 | "commitizen": "^4.3.0", 32 | "copy-webpack-plugin": "^12.0.2", 33 | "css-loader": "^7.1.2", 34 | "cssnano": "^7.0.4", 35 | "cz-conventional-changelog": "^3.3.0", 36 | "eslint": "^9.0.0", 37 | "eslint-config-prettier": "^9.1.0", 38 | "eslint-plugin-prettier": "^5.2.1", 39 | "eslint-webpack-plugin": "^4.2.0", 40 | "mini-css-extract-plugin": "^2.9.0", 41 | "mjml": "^4.15.3", 42 | "postcss-custom-properties": "^14.0.0", 43 | "postcss-loader": "^8.1.1", 44 | "prettier": "^3.3.3", 45 | "sass": "~1.32.11", 46 | "sass-loader": "^16.0.0", 47 | "standard-version": "^9.5.0", 48 | "stylelint": "^16.7.0", 49 | "stylelint-config-standard-scss": "^13.1.0", 50 | "stylelint-webpack-plugin": "^5.0.1", 51 | "webpack": "^5.93.0", 52 | "webpack-cli": "^5.1.4", 53 | "webpack-dev-server": "^5.0.4" 54 | }, 55 | "browserslist": { 56 | "production": [ 57 | ">5%", 58 | "last 2 versions", 59 | "not ie > 0", 60 | "not ie_mob > 0", 61 | "Firefox ESR" 62 | ], 63 | "development": [ 64 | "last 1 chrome version", 65 | "last 1 firefox version", 66 | "last 1 edge version" 67 | ] 68 | }, 69 | "config": { 70 | "commitizen": { 71 | "path": "./node_modules/cz-conventional-changelog" 72 | } 73 | }, 74 | "stylelint": { 75 | "extends": [ 76 | "stylelint-config-standard-scss" 77 | ], 78 | "rules": { 79 | "at-rule-no-unknown": null, 80 | "scss/at-rule-no-unknown": true, 81 | "scss/at-import-partial-extension": null 82 | }, 83 | "ignoreFiles": [ 84 | "**/static/**/*.*", 85 | "**/staticfiles/css/*.*" 86 | ] 87 | }, 88 | "standard-version": { 89 | "header": "# Changelog\n\nAll notable changes to this project will be documented here.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project attempts to adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n", 90 | "types": [ 91 | { 92 | "type": "feat", 93 | "section": "🚀 Features" 94 | }, 95 | { 96 | "type": "fix", 97 | "section": "🐛 Bug Fixes" 98 | }, 99 | { 100 | "type": "docs", 101 | "section": "📝 Docs", 102 | "hidden": false 103 | }, 104 | { 105 | "type": "style", 106 | "section": "💄 Styling", 107 | "hidden": false 108 | }, 109 | { 110 | "type": "refactor", 111 | "hidden": false, 112 | "section": "♻️ Code Refactoring" 113 | }, 114 | { 115 | "type": "perf", 116 | "section": "⚡️ Performance Improvements", 117 | "hidden": false 118 | }, 119 | { 120 | "type": "test", 121 | "section": "✅ Tests", 122 | "hidden": false 123 | }, 124 | { 125 | "type": "build", 126 | "section": "⚙️ Build System", 127 | "hidden": false 128 | }, 129 | { 130 | "type": "ci", 131 | "section": "👷 CI/CD", 132 | "hidden": false 133 | }, 134 | { 135 | "type": "chore", 136 | "section": "🚧 Others", 137 | "hidden": true 138 | }, 139 | { 140 | "type": "revert", 141 | "section": "⏪️ Reverts", 142 | "hidden": true 143 | } 144 | ], 145 | "scripts": { 146 | "prechangelog": "sed -e '1,6d' -i CHANGELOG.md", 147 | "postchangelog": "sed -e 's/###\\ \\[/##\\ \\[v/g' -i CHANGELOG.md && sed -re 's/##\\ \\[([0-9])/##\\ \\[v\\1/g' -i CHANGELOG.md" 148 | } 149 | }, 150 | "engines": { 151 | "node": ">= 22 <23", 152 | "npm": ">= 10" 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "poetry.core.masonry.api" 3 | requires = ["poetry-core"] 4 | 5 | [tool.poetry] 6 | authors = ["{{ cookiecutter.author_name }} <{{ cookiecutter.email }}>"] 7 | description = "{{ cookiecutter.description }}" 8 | name = "{{cookiecutter.project_slug}}" 9 | packages = [{ include = "{{cookiecutter.project_slug}}" }] 10 | readme = "README.md" 11 | version = "0.0.0" 12 | 13 | [tool.poetry.dependencies] 14 | # Core 15 | python = "~=3.12" 16 | django = { version = ">=5.1,<5.2", extras = ["argon2", "bcrypt"] } 17 | wagtail = ">=6.1,<6.2" 18 | psycopg2 = "^2.9.9" 19 | 20 | # Django Extensions 21 | crispy-bootstrap5 = "^2024.2" 22 | django-crispy-forms = "^2.1" 23 | django-environ = "0.11.2" 24 | django-extensions = "3.2.3" 25 | django-mjml = "1.3" 26 | django-redis = "5.4.0" 27 | django-rq = "^2.10.1" 28 | django-widget-tweaks = "1.5.0" 29 | 30 | # Wagtail Extensions 31 | wagtail-font-awesome-svg = "^1.0.1" 32 | 33 | # Other third-party libraries 34 | bpython = "^0.24" 35 | hiredis = "^2.3.2" 36 | pydantic = "^2.5.3" 37 | whitenoise = "^6.6.0" 38 | 39 | # Production 40 | boto3 = "^1.34.7" 41 | # remove the extras you don't need 42 | django-anymail = {extras = ["amazon-ses", "mailersend", "mailgun", "mailjet", "mandrill", "postal", "postmark", "resend", "sendgrid", "sendinblue", "sparkpost"], version = "^11.0.1"} 43 | django-storages = "^1.14.2" 44 | gunicorn = "^22.0.0" 45 | pymemcache = "4.0.0" 46 | sentry-sdk = "^2.10.0" 47 | 48 | [tool.poetry.group.dev] 49 | optional = true 50 | 51 | [tool.poetry.group.dev.dependencies] 52 | black = "^24.4.2" 53 | commitizen = "^3.14.1" 54 | django-debug-toolbar = "3.2.2" 55 | djlint = "^1.34.1" 56 | dslr = "^0.4.0" 57 | honcho = "^2.0.0" 58 | invoke = "^2.2.0" 59 | pre-commit = "3.8.0" 60 | ruff = "^0.7.0" 61 | wagtail-factories = "^4.1.0" 62 | 63 | [tool.poetry.group.test] 64 | optional = true 65 | 66 | [tool.poetry.group.test.dependencies] 67 | pytest-cov = "^4.1.0" 68 | pytest-django = "^4.7.0" 69 | pytest-dotenv = "^0.5.2" 70 | pytest-factoryboy = "^2.6.0" 71 | pytest-logger = "^0.5.1" 72 | pytest-mock = "^3.12.0" 73 | pytest-sugar = "^0.9.7" 74 | pytest-xdist = "^3.5.0" 75 | 76 | [tool.poetry.group.docs] 77 | optional = true 78 | 79 | [tool.poetry.group.docs.dependencies] 80 | mkdocs = "^1.5.3" 81 | mkdocs-git-revision-date-localized-plugin = "^1.2.1" 82 | mkdocs-glightbox = "^0.4.0" 83 | mkdocs-material = "^9.5.2" 84 | 85 | [tool.black] 86 | exclude = ''' 87 | 88 | ( 89 | /( 90 | \.eggs # exclude a few common directories in the 91 | | \.git # root of the project 92 | | \.hg 93 | | \.mypy_cache 94 | | \.tox 95 | | \.venv 96 | | _build 97 | | buck-out 98 | | build 99 | | (.*)/migrations 100 | | node_modules 101 | | dist 102 | )/ 103 | ) 104 | ''' 105 | include = '\.pyi?$' 106 | line-length = 120 107 | target-version = ["py38", "py39", "py310", "py311", "py312"] 108 | 109 | [tool.ruff] 110 | exclude = [".git", "__pycache__", "node_modules", "public", "venv", ".venv"] 111 | line-length = 120 112 | target-version = "py312" 113 | 114 | [tool.ruff.lint] 115 | ignore = ["E203", "E266", "E501"] 116 | select = ["B", "C", "E", "F", "W", "B9"] 117 | 118 | [tool.ruff.lint.isort] 119 | known-first-party = ["{{cookiecutter.project_slug}}"] 120 | section-order = [ 121 | "future", 122 | "standard-library", 123 | "third-party", 124 | "first-party", 125 | "local-folder", 126 | ] 127 | 128 | [tool.ruff.lint.pycodestyle] 129 | max-doc-length = 120 130 | 131 | [tool.ruff.lint.mccabe] 132 | max-complexity = 10 133 | 134 | [tool.coverage.run] 135 | branch = true # Measure branch coverage 136 | omit = [ 137 | "**/migrations/*", 138 | "*tests*", 139 | "**/settings/*", 140 | "*urls.py", 141 | "*wsgi.py", 142 | ] 143 | source = ["{{cookiecutter.project_slug}}"] 144 | 145 | [tool.coverage.report] 146 | show_missing = true 147 | skip_covered = true 148 | 149 | [tool.pytest.ini_options] 150 | DJANGO_SETTINGS_MODULE = "{{cookiecutter.project_slug}}.settings.test" 151 | addopts = "--ds={{cookiecutter.project_slug}}.settings.test -s -vv --cov-config=pyproject.toml --cov --cov-report json --cov-report term-missing:skip-covered" 152 | env_override_existing_values = 1 153 | log_cli = 1 154 | python_files = ["test_*.py", "*_tests.py"] 155 | 156 | [tool.commitizen] 157 | annotated_tag = true 158 | tag_format = "v$major.$minor.$patch" 159 | update_changelog_on_bump = false 160 | version_files = [ 161 | "{{cookiecutter.project_slug}}/__init__.py", 162 | ] 163 | version_provider = "poetry" 164 | 165 | [tool.djlint] 166 | custom_html = "mjml,mj-\\w+" 167 | profile = "django" 168 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/webpack.config.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const autoprefixer = require("autoprefixer"); 4 | const cssnano = require("cssnano"); 5 | const CopyPlugin = require("copy-webpack-plugin"); 6 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 7 | const postcssCustomProperties = require("postcss-custom-properties"); 8 | const sass = require("sass"); 9 | const ESLintPlugin = require("eslint-webpack-plugin"); 10 | const StylelintPlugin = require("stylelint-webpack-plugin"); 11 | 12 | const projectRoot = "{{cookiecutter.project_slug}}"; 13 | 14 | // We are porting gulp-npm-dist to webpack 15 | // https://github.com/dshemendiuk/gulp-npm-dist/blob/3eca60bdf8cfa85295cbd3cf302ce060a9b32725/index.js 16 | const excludePatterns = [ 17 | "*.map", 18 | "sandbox/**/*", 19 | "src/", 20 | "src/**/*", 21 | "examples/", 22 | "examples/**/*", 23 | "example/", 24 | "example/**/*", 25 | "demo/**/*", 26 | "spec/", 27 | "spec/**/*", 28 | "docs/", 29 | "docs/**/*", 30 | "tests/", 31 | "tests/**/*", 32 | "test/", 33 | "test/**/*", 34 | "Gruntfile.js", 35 | "gulpfile.js", 36 | "package.json", 37 | "package-lock.json", 38 | "bower.json", 39 | "composer.json", 40 | "yarn.lock", 41 | "webpack.config.js", 42 | "README", 43 | "LICENSE", 44 | "CHANGELOG", 45 | "*.yml", 46 | "*.md", 47 | "*.coffee", 48 | "*.ts", 49 | "*.scss", 50 | "*.less", 51 | ]; 52 | 53 | const vendorLibrariesToCopy = (config) => { 54 | config = config || {}; 55 | 56 | const copyUnminified = config.copyUnminified || false; 57 | const replaceDefaultExcludes = config.replaceDefaultExcludes || false; 58 | const nodeModulesPath = config.nodeModulesPath || false; 59 | const packageJsonPath = config.packageJsonPath || false; 60 | let excludes = config.excludes || []; 61 | 62 | const workingDir = process.cwd(); 63 | const nodeModDir = nodeModulesPath 64 | ? path.join(workingDir, nodeModulesPath) 65 | : "."; 66 | const packageJsonFile = packageJsonPath 67 | ? path.join(workingDir, packageJsonPath, "package.json") 68 | : "package.json"; 69 | 70 | const buffer = fs.readFileSync(packageJsonFile); 71 | const packageJson = JSON.parse(buffer.toString().trim()); 72 | const packages = []; 73 | 74 | if (!replaceDefaultExcludes) { 75 | excludes = excludes.concat(excludePatterns); 76 | } 77 | 78 | for (lib in packageJson.dependencies) { 79 | var mainFileDir = path.join(nodeModDir, "node_modules", lib); 80 | var libFiles = []; 81 | 82 | if (fs.existsSync(mainFileDir + "/dist")) { 83 | mainFileDir = mainFileDir + "/dist"; 84 | } else { 85 | var depPackageBuffer = fs.readFileSync(mainFileDir + "/package.json"); 86 | var depPackage = JSON.parse(depPackageBuffer.toString()); 87 | 88 | if (depPackage.main) { 89 | var mainFile = mainFileDir + "/" + depPackage.main; 90 | var distDirPos; 91 | 92 | distDirPos = mainFile.lastIndexOf("/dist/"); 93 | 94 | if (distDirPos !== -1) { 95 | mainFileDir = mainFile.substring(0, distDirPos) + "/dist"; 96 | } 97 | } 98 | } 99 | 100 | function readLibFilesRecursively(target) { 101 | try { 102 | fs.readdirSync(target).forEach(function (path) { 103 | var fullPath = target + "/" + path; 104 | if (fs.lstatSync(fullPath).isDirectory()) { 105 | readLibFilesRecursively(fullPath); 106 | } 107 | libFiles.push(fullPath); 108 | }); 109 | } catch (err) { 110 | console.log(err); 111 | } 112 | } 113 | 114 | readLibFilesRecursively(mainFileDir); 115 | 116 | // Includes 117 | packages.push(mainFileDir + "/**/*"); 118 | 119 | //Excludes 120 | excludes.map(function (value) { 121 | packages.push("!" + mainFileDir + "/**/" + value); 122 | }); 123 | 124 | if (copyUnminified === false) { 125 | // Delete unminified versions 126 | for (var i = 0; i < libFiles.length; i++) { 127 | var target; 128 | if (libFiles[i].indexOf(".min.js") > -1) { 129 | target = libFiles[i].replace(/\.min\.js/, ".js"); 130 | packages.push("!" + libFiles[libFiles.indexOf(target)]); 131 | } 132 | if (libFiles[i].indexOf(".min.css") > -1) { 133 | target = libFiles[i].replace(/\.min\.css/, ".css"); 134 | packages.push("!" + libFiles[libFiles.indexOf(target)]); 135 | } 136 | } 137 | } 138 | } 139 | 140 | return packages.filter((item) => !item.startsWith("!")); 141 | }; 142 | 143 | const vendorLibPatterns = vendorLibrariesToCopy().map((library) => { 144 | const from = library.replace( 145 | "node_modules/intl-tel-input/**/*", 146 | "node_modules/intl-tel-input/build/**/*", 147 | ); 148 | const context = process.cwd(); 149 | const to = path.resolve(`./${projectRoot}/static/vendors`); 150 | return { 151 | from, 152 | context, 153 | to({ context, absoluteFilename }) { 154 | // excludes 'node_modules' from path 155 | // we don't want dist folders appearing in destination 156 | return `${to}/${path.relative(context, absoluteFilename)}` 157 | .replace("node_modules", "") 158 | .replace(/\/dist/, "") 159 | .replace(/\\dist/, ""); 160 | }, 161 | globOptions: { 162 | ignore: excludePatterns, 163 | }, 164 | }; 165 | }); 166 | 167 | const options = { 168 | entry: { 169 | // multiple entries can be added here 170 | main: `./${projectRoot}/assets/js/main.js`, 171 | }, 172 | output: { 173 | path: path.resolve(`./${projectRoot}/static/`), 174 | // based on entry name, e.g. main.js 175 | filename: "js/[name].min.js", // based on entry name, e.g. main.js 176 | clean: true, 177 | }, 178 | plugins: [ 179 | new CopyPlugin({ 180 | patterns: [ 181 | { 182 | // Copy images to be referenced directly by Django to the "img" subfolder in static files. 183 | from: "img", 184 | context: path.resolve(`./${projectRoot}/assets/`), 185 | to: path.resolve(`./${projectRoot}/static/img`), 186 | }, 187 | { 188 | // Copy favicons to be referenced directly by Django to the "ico" subfolder in static files. 189 | from: "ico", 190 | context: path.resolve(`./${projectRoot}/assets/`), 191 | to: path.resolve(`./${projectRoot}/static/ico`), 192 | }, 193 | // Copy package.json deps to be referenced directly by Django to the "vendors" subfolder in static files. 194 | ...vendorLibPatterns, 195 | ], 196 | }), 197 | new MiniCssExtractPlugin({ 198 | filename: "css/[name].min.css", 199 | }), 200 | new ESLintPlugin({ 201 | failOnError: false, 202 | lintDirtyModulesOnly: true, 203 | emitWarning: true, 204 | }), 205 | new StylelintPlugin({ 206 | failOnError: false, 207 | lintDirtyModulesOnly: true, 208 | emitWarning: true, 209 | extensions: ["scss"], 210 | }), 211 | ], 212 | module: { 213 | rules: [ 214 | { 215 | test: /\.js$/, 216 | exclude: /node_modules/, 217 | use: { 218 | loader: "babel-loader", 219 | }, 220 | }, 221 | { 222 | // this will apply to `.sass` / `.scss` / `.css` files 223 | test: /\.(s[ac]ss|css)$/, 224 | use: [ 225 | MiniCssExtractPlugin.loader, 226 | { 227 | loader: "css-loader", 228 | options: { 229 | sourceMap: true, 230 | }, 231 | }, 232 | { 233 | loader: "postcss-loader", 234 | options: { 235 | sourceMap: true, 236 | postcssOptions: { 237 | plugins: () => [ 238 | autoprefixer(), 239 | postcssCustomProperties(), 240 | cssnano({ 241 | preset: "default", 242 | }), 243 | ], 244 | }, 245 | }, 246 | }, 247 | { 248 | loader: "sass-loader", 249 | options: { 250 | sourceMap: true, 251 | implementation: sass, 252 | sassOptions: { 253 | outputStyle: "compressed", 254 | }, 255 | }, 256 | }, 257 | ], 258 | }, 259 | { 260 | // sync font files referenced by the css to the fonts directory 261 | // the publicPath matches the path from the compiled css to the font file 262 | test: /\.(woff|woff2)$/, 263 | include: /fonts/, 264 | type: "asset/resource", 265 | generator: { 266 | filename: "fonts/[name][ext]", 267 | }, 268 | }, 269 | ], 270 | }, 271 | // externals are loaded via base.html and not included in the webpack bundle. 272 | externals: { 273 | // gettext: 'gettext', 274 | }, 275 | }; 276 | 277 | /* 278 | If a project requires internationalisation, then include `gettext` in base.html 279 | via the Django JSi18n helper, and uncomment it from the 'externals' object above. 280 | */ 281 | 282 | const webpackConfig = (environment, argv) => { 283 | const isProduction = argv.mode === "production"; 284 | 285 | options.mode = isProduction ? "production" : "development"; 286 | 287 | if (!isProduction) { 288 | // https://webpack.js.org/configuration/stats/ 289 | const stats = { 290 | // Tells stats whether to add the build date and the build time information. 291 | builtAt: false, 292 | // Add chunk information (setting this to `false` allows for a less verbose output) 293 | chunks: false, 294 | // Add the hash of the compilation 295 | hash: false, 296 | // `webpack --colors` equivalent 297 | colors: true, 298 | // Add information about the reasons why modules are included 299 | reasons: false, 300 | // Add webpack version information 301 | version: false, 302 | // Add built modules information 303 | modules: false, 304 | // Show performance hint when file size exceeds `performance.maxAssetSize` 305 | performance: false, 306 | // Add children information 307 | children: false, 308 | // Add asset Information. 309 | assets: false, 310 | }; 311 | 312 | options.stats = stats; 313 | 314 | // Create JS source maps in the dev mode 315 | // See https://webpack.js.org/configuration/devtool/ for more options 316 | options.devtool = "inline-source-map"; 317 | 318 | // See https://webpack.js.org/configuration/dev-server/. 319 | options.devServer = { 320 | // Enable gzip compression for everything served. 321 | compress: true, 322 | hot: false, 323 | client: { 324 | logging: "error", 325 | // Shows a full-screen overlay in the browser when there are compiler errors. 326 | overlay: true, 327 | }, 328 | static: false, 329 | host: "0.0.0.0", 330 | allowedHosts: ["all"], 331 | port: 3000, 332 | devMiddleware: { 333 | index: false, // specify to enable root proxying 334 | publicPath: "/static/", 335 | // Write compiled files to disk. This makes live-reload work on both port 3000 and 8000. 336 | writeToDisk: true, 337 | }, 338 | proxy: [ 339 | { 340 | context: () => true, 341 | target: "http://web:8000", 342 | }, 343 | ], 344 | }; 345 | } 346 | 347 | return options; 348 | }; 349 | 350 | module.exports = webpackConfig; 351 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.2.0" 2 | __version_info__ = tuple([int(num) if num.isdigit() else num for num in __version__.replace("-", ".", 1).split(".")]) 3 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/assets/ico/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engineervix/cookiecutter-wagtail-vix/66d5ca64237bd487092b2a65986a9254a693a008/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/assets/ico/android-chrome-192x192.png -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/assets/ico/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engineervix/cookiecutter-wagtail-vix/66d5ca64237bd487092b2a65986a9254a693a008/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/assets/ico/android-chrome-512x512.png -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/assets/ico/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engineervix/cookiecutter-wagtail-vix/66d5ca64237bd487092b2a65986a9254a693a008/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/assets/ico/apple-touch-icon.png -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/assets/ico/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engineervix/cookiecutter-wagtail-vix/66d5ca64237bd487092b2a65986a9254a693a008/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/assets/ico/favicon-16x16.png -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/assets/ico/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engineervix/cookiecutter-wagtail-vix/66d5ca64237bd487092b2a65986a9254a693a008/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/assets/ico/favicon-32x32.png -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/assets/ico/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engineervix/cookiecutter-wagtail-vix/66d5ca64237bd487092b2a65986a9254a693a008/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/assets/ico/favicon.ico -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/assets/ico/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/assets/img/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 9 | 10 | 11 | 14 | 17 | 18 | 20 | 21 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/assets/img/warning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/assets/js/main.js: -------------------------------------------------------------------------------- 1 | import "../scss/main.scss"; 2 | 3 | // ======================================================= 4 | // Enable tooltips 5 | // https://getbootstrap.com/docs/5.3/components/tooltips/#enable-tooltips 6 | // ======================================================= 7 | 8 | const tooltipTriggerList = document.querySelectorAll( 9 | '[data-bs-toggle="tooltip"]', 10 | ); 11 | const tooltipList = [...tooltipTriggerList].map( 12 | (tooltipTriggerEl) => new bootstrap.Tooltip(tooltipTriggerEl), 13 | ); 14 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/assets/scss/abstracts/_custom_bootstrap_vars.scss: -------------------------------------------------------------------------------- 1 | $primary: #208152; 2 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/assets/scss/abstracts/_variables.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // This file contains all application-wide Sass variables. 3 | // ----------------------------------------------------------------------------- 4 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/assets/scss/base/_base.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // This file contains very basic styles. 3 | // ----------------------------------------------------------------------------- 4 | 5 | /* Border box declaration 6 | https://www.paulirish.com/2012/box-sizing-border-box-ftw/ */ 7 | html { 8 | box-sizing: border-box; 9 | } 10 | 11 | /* inherit border-box on all elements in the universe and before and after */ 12 | *, 13 | *::before, 14 | *::after { 15 | box-sizing: inherit; 16 | } 17 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/assets/scss/base/_typography.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // Basic typography style for copy text 3 | // ----------------------------------------------------------------------------- 4 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/assets/scss/components/_navbar.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // This file contains all styles related to the navbar component. 3 | // ----------------------------------------------------------------------------- 4 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/assets/scss/layout/_footer.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // This file contains all styles related to the footer of the site/application. 3 | // ----------------------------------------------------------------------------- 4 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/assets/scss/layout/_header.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // This file contains all styles related to the header of the site/application. 3 | // ----------------------------------------------------------------------------- 4 | 5 | // we use this to shift the content down when the navbar is fixed to the top 6 | // in order to prevent overlap. 7 | // See: https://getbootstrap.com/docs/5.3/components/navbar/#placement 8 | // and https://getbootstrap.com/docs/5.3/components/navbar/#sass-variables 9 | $navbar-height: $nav-link-height + $navbar-padding-y + $navbar-toggler-padding-y; 10 | 11 | .hero { 12 | &--home { 13 | // Styles for hero--home 14 | 15 | nav.navbar { 16 | // Styles for the navbar 17 | 18 | ~ div.container { 19 | margin-top: $navbar-height; 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/assets/scss/main.scss: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | // This is *roughly* based on the [7-1 pattern](https://sass-guidelin.es/). 4 | 5 | // 1. Configuration and helpers 6 | // 2. Vendors 7 | 8 | @import "bourbon"; 9 | 10 | // https://getbootstrap.com/docs/5.3/customize/sass/ 11 | 12 | // Custom Bootstrap Variables 13 | @import "abstracts/custom_bootstrap_vars"; 14 | 15 | // Set your own initial variables 16 | @import "abstracts/variables"; 17 | 18 | // 2. Vendors: Bootstrap 19 | @import "../../../node_modules/bootstrap/scss/bootstrap"; 20 | 21 | // 2. Vendors: Custom vendor overrides 22 | 23 | @import "vendors/wagtail"; 24 | 25 | // 3. Base stuff 26 | @import "base/base"; 27 | @import "base/typography"; 28 | 29 | // 4. Layout-related sections 30 | @import "layout/header"; 31 | @import "layout/footer"; 32 | 33 | // 5. Components 34 | @import "components/navbar"; 35 | 36 | // 6. Page-specific styles 37 | @import "pages/home"; 38 | 39 | // 7. Themes 40 | @import "themes/dark"; 41 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/assets/scss/pages/_home.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // This file contains styles that are specific to the home page. 3 | // ----------------------------------------------------------------------------- 4 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/assets/scss/themes/_dark.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // When having several themes, this file contains everything related to the 3 | // dark theme 4 | // ----------------------------------------------------------------------------- 5 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/assets/scss/vendors/_wagtail.scss: -------------------------------------------------------------------------------- 1 | /* Responsive Wagtail Embeds 2 | * https://docs.wagtail.org/en/v5.2/topics/writing_templates.html#responsive-embeds 3 | * ------------------------------------------------- 4 | */ 5 | 6 | .responsive-object { 7 | position: relative; 8 | } 9 | 10 | .responsive-object iframe, 11 | .responsive-object object, 12 | .responsive-object embed { 13 | height: 100%; 14 | left: 0; 15 | position: absolute; 16 | top: 0; 17 | width: 100%; 18 | } 19 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture(autouse=True) 5 | def media_storage(settings, tmpdir): 6 | settings.MEDIA_ROOT = tmpdir.strpath 7 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engineervix/cookiecutter-wagtail-vix/66d5ca64237bd487092b2a65986a9254a693a008/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/core/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoreConfig(AppConfig): 5 | name = "{{cookiecutter.project_slug}}.core" 6 | label = "core" 7 | verbose_name = "Core" 8 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/core/templates/core/search.html: -------------------------------------------------------------------------------- 1 | {% raw %}{% extends "base.html" %} 2 | {% load static wagtailcore_tags wagtailimages_tags %} 3 | {% block title %} 4 | {{ title }} 5 | {% endblock title %} 6 | {% block extrameta %} 7 | {% endraw %} 8 | 9 | {% raw %} 10 | 11 | {% endraw %} 12 | {% raw %} 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% endblock extrameta %} 20 | {% block body_class %} 21 | template--search 22 | {% endblock body_class %} 23 | {% block header_class %} 24 | hero--search bg-dark text-light 25 | {% endblock header_class %} 26 | {% block promo_block %} 27 |
28 |
29 |
30 |

31 | Search 32 | {% if search_results %}Results{% endif %} 33 |

34 |

Needle in a haystack is super easy - just bring a powerful magnet.

35 |
36 |
37 |
38 | {% endblock promo_block %} 39 | {% block content %} 40 |
41 |
42 |

Search Results

43 |
    44 | {% if search_results %} 45 | {% for result in search_results %} 46 |
  • 47 |
    48 |
    49 | {# https://www.svgrepo.com/svg/3907/search #} 50 | {# djlint:off H006 #} 51 | search result 55 | {# djlint:on #} 56 |
    57 |
    58 |
    59 | {{ result }} 60 |
    61 | {% if result.specific.summary %} 62 |

    {{ result.specific.summary|striptags|truncatechars:150 }}

    63 | {% elif result.specific.introduction %} 64 |

    {{ result.specific.introduction|striptags|truncatechars:150 }}

    65 | {% elif result.specific.content %} 66 |

    {{ result.specific.content|striptags|truncatechars:150 }}

    67 | {% endif %} 68 | {% if result.last_published_at %} 69 |

    Last updated: {{ result.last_published_at }}

    70 | {% endif %} 71 |
    72 |
    73 |
  • 74 | {% endfor %} 75 | {% elif search_query %} 76 |
  • 77 |
    78 |
    79 | {# https://www.svgrepo.com/svg/80831/warning #} 80 | {# djlint:off H006 #} 81 | no results found 85 | {# djlint:on #} 86 |
    87 |
    88 |

    No results found

    89 |
    90 |
    91 |
  • 92 | {% else %} 93 |
  • 94 |
    95 |
    96 | {# https://www.svgrepo.com/svg/80831/warning #} 97 | {# djlint:off H006 #} 98 | no search term 102 | {# djlint:on #} 103 |
    104 |
    105 |

    Please enter a search term

    106 |
    107 |
    108 |
  • 109 | {% endif %} 110 |
111 | {# Only show pagination if there is more than one page to click through #} 112 | {% if search_results.paginator.num_pages > 1 %} 113 |
114 |
115 | 145 |
146 |
147 | {% endif %} 148 |
149 |
150 | {% endblock content %}{% endraw %} 151 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/core/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engineervix/cookiecutter-wagtail-vix/66d5ca64237bd487092b2a65986a9254a693a008/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/core/tests/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/core/tests/test_search.py: -------------------------------------------------------------------------------- 1 | import wagtail_factories 2 | from django.test import TestCase 3 | from django.urls import reverse 4 | from wagtail.models import Page, Site 5 | 6 | from {{cookiecutter.project_slug}}.home.factories import HomePageFactory 7 | 8 | 9 | class SiteSearchTestCase(TestCase): 10 | @classmethod 11 | def setUpTestData(cls): 12 | root = wagtail_factories.PageFactory(parent=None) 13 | cls.page = HomePageFactory(parent=root) 14 | 15 | hostname = "example.com" 16 | Site.objects.all().delete() 17 | Site.objects.create( 18 | hostname=hostname, 19 | root_page=cls.page, 20 | site_name="Test Site", 21 | is_default_site=True, 22 | ) 23 | 24 | def test_site_search_with_results(self): 25 | # Create a search query 26 | search_query = "Home" 27 | 28 | # Simulate a GET request to the search view 29 | response = self.client.get(reverse("search"), {"query": search_query}) 30 | 31 | # Check that the response has a 200 status code 32 | self.assertEqual(response.status_code, 200) 33 | 34 | # Check that the search results are present in the response context 35 | self.assertIn("search_results", response.context) 36 | 37 | # Check that the search query is present in the response context 38 | self.assertEqual(response.context["search_query"], search_query) 39 | 40 | def test_site_search_without_results(self): 41 | # Create a search query that will not yield any results 42 | search_query = "Lorem Ipsum" 43 | 44 | # Simulate a GET request to the search view 45 | response = self.client.get(reverse("search"), {"query": search_query}) 46 | 47 | # Check that the response has a 200 status code 48 | self.assertEqual(response.status_code, 200) 49 | 50 | # Check that the search results are an empty queryset 51 | self.assertQuerySetEqual(response.context["search_results"], Page.objects.none()) 52 | 53 | # Check that the search query is present in the response context 54 | self.assertEqual(response.context["search_query"], search_query) 55 | 56 | def test_site_search_pagination(self): 57 | # Create more pages to ensure pagination is working 58 | for i in range(15): 59 | HomePageFactory(title=f"Test Page {i + 3}", slug=f"test-page-{i + 3}") 60 | 61 | # Create a search query 62 | search_query = "Test" 63 | 64 | # Simulate a GET request to the search view with a specific page number 65 | response = self.client.get(reverse("search"), {"query": search_query, "page": 2}) 66 | 67 | # Check that the response has a 200 status code 68 | self.assertEqual(response.status_code, 200) 69 | 70 | # Check that the correct page of search results is present in the response context 71 | self.assertEqual(response.context["search_results"].number, 2) 72 | 73 | def test_site_search_page_not_an_integer(self): 74 | # Simulate a GET request to the search view with a non-integer page value 75 | response = self.client.get(reverse("search"), {"query": "Test", "page": "not_an_integer"}) 76 | 77 | # Check that the response has a 200 status code 78 | self.assertEqual(response.status_code, 200) 79 | 80 | # Check that the search results are from the first page 81 | self.assertEqual(response.context["search_results"].number, 1) 82 | 83 | def test_site_search_empty_page(self): 84 | # Create more pages to ensure pagination is working 85 | for i in range(15): 86 | HomePageFactory(title=f"Test Page {i + 3}", slug=f"test-page-{i + 3}") 87 | 88 | # Simulate a GET request to the search view with a page number exceeding the available pages 89 | response = self.client.get(reverse("search"), {"query": "Test", "page": 1000}) 90 | 91 | # Check that the response has a 200 status code 92 | self.assertEqual(response.status_code, 200) 93 | 94 | # Check that the search results are from the last available page 95 | self.assertEqual( 96 | response.context["search_results"].number, response.context["search_results"].paginator.num_pages 97 | ) 98 | 99 | def test_site_search_template_used(self): 100 | # Create a search query 101 | search_query = "Test" 102 | 103 | # Simulate a GET request to the search view 104 | response = self.client.get(reverse("search"), {"query": search_query}) 105 | 106 | # Check that the correct template is used 107 | self.assertTemplateUsed(response, "core/search.html") 108 | 109 | def test_site_search_without_query(self): 110 | # Simulate a GET request to the search view without a search query 111 | response = self.client.get(reverse("search")) 112 | 113 | # Check that the response has a 200 status code 114 | self.assertEqual(response.status_code, 200) 115 | 116 | # Check that the search results are an empty queryset 117 | self.assertQuerySetEqual(response.context["search_results"], Page.objects.none()) 118 | 119 | # Check that the search query in the response context is None 120 | self.assertIsNone(response.context["search_query"]) 121 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/core/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator 3 | from django.template.response import TemplateResponse 4 | from wagtail.contrib.search_promotions.models import Query 5 | from wagtail.models import Page 6 | 7 | 8 | def site_search(request): 9 | search_query = request.GET.get("query", None) 10 | page = request.GET.get("page", request.GET.get("p", 1)) 11 | 12 | # Search 13 | if search_query: 14 | search_results = Page.objects.live().search(search_query) 15 | # Log the query so Wagtail can suggest promoted results 16 | Query.get(search_query).add_hit() 17 | else: 18 | search_results = Page.objects.none() 19 | 20 | # Pagination 21 | results_per_page = 12 22 | paginator = Paginator(search_results, results_per_page) 23 | try: 24 | search_results = paginator.page(page) 25 | except PageNotAnInteger: 26 | search_results = paginator.page(1) 27 | except EmptyPage: 28 | search_results = paginator.page(paginator.num_pages) 29 | 30 | return TemplateResponse( 31 | request, 32 | "core/search.html", 33 | { 34 | "search_query": search_query, 35 | "search_results": search_results, 36 | "title": "{} Search Results for: {}".format(settings.WAGTAIL_SITE_NAME, search_query), 37 | }, 38 | ) 39 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/core/wagtail_hooks.py: -------------------------------------------------------------------------------- 1 | from wagtail import hooks 2 | 3 | 4 | @hooks.register("register_icons") 5 | def register_icons(icons): 6 | return icons + [ 7 | "wagtailfontawesomesvg/regular/newspaper.svg", 8 | "wagtailfontawesomesvg/solid/bullhorn.svg", 9 | "wagtailfontawesomesvg/solid/download.svg", 10 | "wagtailfontawesomesvg/solid/location-dot.svg", 11 | ] 12 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/files/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore everything in this directory 2 | * 3 | # Except this file 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/home/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engineervix/cookiecutter-wagtail-vix/66d5ca64237bd487092b2a65986a9254a693a008/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/home/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/home/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class HomeConfig(AppConfig): 5 | name = "{{cookiecutter.project_slug}}.home" 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/home/factories.py: -------------------------------------------------------------------------------- 1 | import wagtail_factories 2 | 3 | from {{cookiecutter.project_slug}}.home.models import HomePage 4 | 5 | 6 | class HomePageFactory(wagtail_factories.PageFactory): 7 | title = "Home" 8 | 9 | class Meta: 10 | model = HomePage 11 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/home/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2023-12-26 01:54 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | initial = True 9 | 10 | dependencies = [ 11 | ("wagtailcore", "0089_log_entry_data_json_null_to_object"), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name="HomePage", 17 | fields=[ 18 | ( 19 | "page_ptr", 20 | models.OneToOneField( 21 | auto_created=True, 22 | on_delete=django.db.models.deletion.CASCADE, 23 | parent_link=True, 24 | primary_key=True, 25 | serialize=False, 26 | to="wagtailcore.page", 27 | ), 28 | ), 29 | ], 30 | options={ 31 | "abstract": False, 32 | }, 33 | bases=("wagtailcore.page",), 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/home/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engineervix/cookiecutter-wagtail-vix/66d5ca64237bd487092b2a65986a9254a693a008/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/home/migrations/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/home/models.py: -------------------------------------------------------------------------------- 1 | from wagtail.models import Page 2 | 3 | 4 | class HomePage(Page): 5 | pass 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/home/templates/home/home_page.html: -------------------------------------------------------------------------------- 1 | {% raw %}{% extends "base.html" %} 2 | {% load static %} 3 | {% load wagtailcore_tags wagtailimages_tags %} 4 | {% block extrameta %}{% endraw %} 5 | 6 | {% raw %} 7 | 8 | {% endraw %} 9 | 10 | {% raw %} 11 | 12 | 13 | 14 | 15 | 16 | 17 | {% endblock extrameta %} 18 | {% block extra_css %} 19 | {% endblock extra_css %} 20 | {% block body_class %} 21 | template--home 22 | {% endblock body_class %} 23 | {% block header_class %} 24 | hero--home bg-dark text-light 25 | {% endblock header_class %} 26 | {% block promo_block %} 27 |
28 |
29 |
{% endraw %} 30 |

Welcome to {{ cookiecutter.project_name }} Homepage

{% raw %} 31 |

Lorem ipsum dolor sit amet, consectetur adipisicing elit.

32 |
33 |
34 |
35 | {% endblock promo_block %} 36 | {% block content %} 37 |
38 |
39 |
40 |
41 |

Let's get to work ⚒️

42 |
43 |
44 |
45 |
46 | {% endblock content %}{% endraw %} 47 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/home/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engineervix/cookiecutter-wagtail-vix/66d5ca64237bd487092b2a65986a9254a693a008/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/home/tests/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/home/tests/test_factories.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from {{cookiecutter.project_slug}}.home.factories import HomePageFactory 4 | from {{cookiecutter.project_slug}}.home.models import HomePage 5 | 6 | 7 | class HomePageFactoryTestCase(TestCase): 8 | def test_create(self): 9 | assert HomePage.objects.count() == 0 10 | 11 | home = HomePageFactory() 12 | 13 | assert isinstance(home, HomePage) 14 | assert HomePage.objects.count() == 1 15 | assert home.title == "Home" 16 | assert home.slug == "home" 17 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/home/tests/test_models.py: -------------------------------------------------------------------------------- 1 | import wagtail_factories 2 | from wagtail.models import Site 3 | from wagtail.test.utils import WagtailPageTestCase 4 | 5 | from {{cookiecutter.project_slug}}.home.factories import HomePageFactory 6 | 7 | 8 | class HomePageTestCase(WagtailPageTestCase): 9 | @classmethod 10 | def setUpTestData(cls): 11 | root = wagtail_factories.PageFactory(parent=None) 12 | cls.page = HomePageFactory(parent=root) 13 | 14 | hostname = "example.com" 15 | Site.objects.all().delete() 16 | Site.objects.create( 17 | hostname=hostname, 18 | root_page=cls.page, 19 | site_name="Test Site", 20 | is_default_site=True, 21 | ) 22 | 23 | def test_route(self): 24 | self.assertPageIsRoutable(self.page) 25 | 26 | def test_rendering(self): 27 | self.assertPageIsRenderable(self.page) 28 | 29 | def test_editability(self): 30 | self.assertPageIsEditable(self.page) 31 | 32 | def test_general_previewability(self): 33 | self.assertPageIsPreviewable(self.page) 34 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engineervix/cookiecutter-wagtail-vix/66d5ca64237bd487092b2a65986a9254a693a008/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/settings/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/settings/dev.py: -------------------------------------------------------------------------------- 1 | from email.utils import formataddr 2 | 3 | from .base import * # noqa: F403 4 | 5 | # CACHES 6 | # ------------------------------------------------------------------------------ 7 | # https://docs.djangoproject.com/en/5.0/ref/settings/#caches 8 | CACHES = { 9 | "default": { 10 | "BACKEND": "django.core.cache.backends.db.DatabaseCache", 11 | "LOCATION": "database_cache", 12 | } 13 | } 14 | 15 | # EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" 16 | EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" 17 | 18 | # https://docs.djangoproject.com/en/5.0/ref/settings/#email-host 19 | EMAIL_HOST = "maildev" 20 | # https://docs.djangoproject.com/en/5.0/ref/settings/#email-port 21 | EMAIL_PORT = 1025 22 | 23 | DEFAULT_FROM_EMAIL = "Do Not Reply " 24 | 25 | ADMINS.append(("Admin", "admin@{{cookiecutter.domain_name}}")) # noqa: F405 26 | LIST_OF_EMAIL_RECIPIENTS += list(map(lambda recipient: formataddr(recipient), ADMINS)) # noqa F405 27 | 28 | # WhiteNoise 29 | # ------------------------------------------------------------------------------ 30 | # http://whitenoise.evans.io/en/latest/django.html#using-whitenoise-in-development 31 | INSTALLED_APPS = ["whitenoise.runserver_nostatic"] + INSTALLED_APPS # noqa F405 32 | 33 | # LOGGING 34 | # ----------------------------------------------------------------------------- 35 | # https://docs.djangoproject.com/en/5.0/ref/settings/#logging 36 | # See https://docs.djangoproject.com/en/5.0/topics/logging for 37 | # more details on how to customize your logging configuration. 38 | LOGGING = { 39 | "version": 1, 40 | "disable_existing_loggers": False, 41 | "formatters": {"verbose": {"format": "%(levelname)s %(asctime)s %(module)s " "%(process)d %(thread)d %(message)s"}}, 42 | "handlers": { 43 | "console": { 44 | "level": "DEBUG", 45 | "class": "logging.StreamHandler", 46 | "formatter": "verbose", 47 | } 48 | }, 49 | "root": {"level": "INFO", "handlers": ["console"]}, 50 | } 51 | 52 | # Django Debug Toolbar 53 | # ------------------------------------------------------------------------------ 54 | # https://github.com/jazzband/django-debug-toolbar 55 | INSTALLED_APPS.append("debug_toolbar") # noqa: F405 # https://github.com/jazzband/django-debug-toolbar 56 | 57 | # Additional middleware introduced by debug toolbar 58 | # insert after first element value. 59 | 60 | # The order of MIDDLEWARE and MIDDLEWARE_CLASSES is important. 61 | # You should include the Debug Toolbar middleware as early as possible in the list. 62 | # However, it must come after any other middleware that encodes the response's content, 63 | # such as GZipMiddleware. 64 | MIDDLEWARE.insert(1, "debug_toolbar.middleware.DebugToolbarMiddleware") # noqa: F405 65 | 66 | INTERNAL_IPS = ["127.0.0.1", "::1"] 67 | 68 | DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": lambda request: DEBUG} # noqa: F405 69 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/settings/production.py: -------------------------------------------------------------------------------- 1 | """See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/""" 2 | 3 | import logging 4 | from email.utils import formataddr, getaddresses 5 | 6 | import sentry_sdk 7 | from sentry_sdk.integrations.django import DjangoIntegration 8 | from sentry_sdk.integrations.logging import LoggingIntegration 9 | from sentry_sdk.integrations.redis import RedisIntegration 10 | 11 | from .base import * # noqa 12 | 13 | DEBUG = False # just to make sure! 14 | 15 | DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60) # noqa F405 16 | 17 | CACHES = { 18 | "default": { 19 | "BACKEND": "django_redis.cache.RedisCache", 20 | "LOCATION": env("REDIS_URL"), # noqa F405 21 | "KEY_PREFIX": env("REDIS_KEY_PREFIX"), # noqa F405 22 | "OPTIONS": { 23 | "CLIENT_CLASS": "django_redis.client.DefaultClient", 24 | # Mimicing memcache behavior. 25 | # https://github.com/jazzband/django-redis#memcached-exceptions-behavior 26 | "IGNORE_EXCEPTIONS": True, 27 | }, 28 | }, 29 | "renditions": { 30 | "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache", 31 | "LOCATION": env("MEMCACHED_URL"), # noqa F405 32 | "TIMEOUT": 600, 33 | }, 34 | } 35 | 36 | # RQ_QUEUES = { 37 | # "default": { 38 | # "USE_REDIS_CACHE": "default", 39 | # }, 40 | # } 41 | 42 | SESSION_ENGINE = "django.contrib.sessions.backends.cache" 43 | SESSION_CACHE_ALIAS = "default" 44 | 45 | # https://docs.wagtail.org/en/latest/reference/settings.html#wagtail-redirects-file-storage 46 | # By default the redirect importer keeps track of the uploaded file as a temp file, 47 | # but on certain environments (load balanced/cloud environments), you cannot 48 | # keep a shared file between environments. 49 | # For those cases you can use the built-in cache to store the file instead. 50 | WAGTAIL_REDIRECTS_FILE_STORAGE = "cache" 51 | 52 | # https://docs.wagtail.org/en/stable/reference/contrib/frontendcache.html#cloudflare 53 | INSTALLED_APPS += ["wagtail.contrib.frontend_cache"] # noqa: F405 54 | WAGTAILFRONTENDCACHE = { 55 | "cloudflare": { 56 | "BACKEND": "wagtail.contrib.frontend_cache.backends.CloudflareBackend", 57 | "BEARER_TOKEN": env("CLOUDFLARE_BEARER_TOKEN"), # noqa F405 58 | "ZONEID": env("CLOUDFLARE_DOMAIN_ZONE_ID"), # noqa F405 59 | }, 60 | } 61 | 62 | # SECURITY 63 | # ------------------------------------------------------------------------------ 64 | 65 | # if the next two settings are controlled by nginx, comment them out 66 | # https://docs.djangoproject.com/en/5.0/ref/settings/#secure-proxy-ssl-header 67 | SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") 68 | # https://docs.djangoproject.com/en/5.0/ref/settings/#secure-ssl-redirect 69 | SECURE_SSL_REDIRECT = env.bool("DJANGO_SECURE_SSL_REDIRECT", default=True) # noqa F405 70 | SESSION_COOKIE_SECURE = True 71 | CSRF_COOKIE_SECURE = True 72 | CSRF_COOKIE_SAMESITE = "None" 73 | SESSION_COOKIE_SAMESITE = "None" 74 | # if the next set of settings are controlled by nginx, comment them out 75 | # https://docs.djangoproject.com/en/5.0/topics/security/#ssl-https 76 | # https://docs.djangoproject.com/en/5.0/ref/settings/#secure-hsts-seconds 77 | # TODO: set this to 60 seconds first and then to 518400 once you prove the former works 78 | # SECURE_HSTS_SECONDS = 518400 79 | # https://docs.djangoproject.com/en/5.0/ref/settings/#secure-hsts-include-subdomains 80 | # SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool("DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS", default=True) # noqa F405 81 | # https://docs.djangoproject.com/en/5.0/ref/settings/#secure-hsts-preload 82 | # SECURE_HSTS_PRELOAD = env.bool("DJANGO_SECURE_HSTS_PRELOAD", default=True) # noqa F405 83 | # https://docs.djangoproject.com/en/5.0/ref/middleware/#x-content-type-options-nosniff 84 | SECURE_CONTENT_TYPE_NOSNIFF = env.bool("DJANGO_SECURE_CONTENT_TYPE_NOSNIFF", default=True) # noqa F405 85 | 86 | # ============================================================================== 87 | # incorporationg Backblaze B2 Cloud Storage 88 | # ref: https://github.com/jschneier/django-storages/issues/765#issuecomment-699487715 89 | # DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' 90 | # AWS_ACCESS_KEY_ID = os.getenv('B2_USER') 91 | # AWS_SECRET_ACCESS_KEY = os.getenv('B2_KEY') 92 | # AWS_STORAGE_BUCKET_NAME = os.getenv('B2_BUCKET') 93 | # AWS_S3_CUSTOM_DOMAIN = 'f002.backblazeb2.com/file/B2_BUCKET' 94 | # AWS_S3_ENDPOINT_URL = 'https://B2_BUCKET.s3.us-west-002.backblazeb2.com' # exact url stated in b2 bucket overview page 95 | # ============================================================================== 96 | 97 | # STORAGES 98 | # ------------------------------------------------------------------------------ 99 | # https://django-storages.readthedocs.io/en/latest/#installation 100 | INSTALLED_APPS += ["storages"] # noqa: F405 101 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 102 | AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID") # noqa: F405 103 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 104 | AWS_SECRET_ACCESS_KEY = env("AWS_SECRET_ACCESS_KEY") # noqa: F405 105 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 106 | AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME") # noqa: F405 107 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 108 | AWS_QUERYSTRING_AUTH = False 109 | AWS_S3_FILE_OVERWRITE = env("AWS_S3_FILE_OVERWRITE", default=False) # noqa: F405 110 | # DO NOT change these unless you know what you're doing. 111 | _AWS_EXPIRY = 60 * 60 * 24 * 7 112 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 113 | AWS_S3_OBJECT_PARAMETERS = {"CacheControl": f"max-age={_AWS_EXPIRY}, s-maxage={_AWS_EXPIRY}, must-revalidate"} 114 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 115 | AWS_S3_REGION_NAME = env("AWS_S3_REGION_NAME", default=None) # noqa: F405 116 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#cloudfront 117 | AWS_S3_CUSTOM_DOMAIN = env("AWS_S3_CUSTOM_DOMAIN", default=None) # noqa: F405 118 | # https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html#settings 119 | AWS_S3_ENDPOINT_URL = env("AWS_S3_ENDPOINT_URL") # noqa: F405 120 | aws_s3_domain = AWS_S3_CUSTOM_DOMAIN or f"{AWS_STORAGE_BUCKET_NAME}.s3.{AWS_S3_REGION_NAME}.backblaze.com" 121 | 122 | # STATIC 123 | # ------------------------ 124 | STORAGES = { 125 | "default": { 126 | "BACKEND": "storages.backends.s3boto3.S3Boto3Storage", 127 | }, 128 | "staticfiles": { 129 | "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", 130 | }, 131 | } 132 | # Temporary hack if you experience problems: https://stackoverflow.com/a/69123932 133 | # STORAGES = { 134 | # "default": { 135 | # "BACKEND": "storages.backends.s3boto3.S3Boto3Storage", 136 | # }, 137 | # "staticfiles": { 138 | # "BACKEND": "whitenoise.storage.CompressedStaticFilesStoragee", 139 | # }, 140 | # } 141 | 142 | # MEDIA 143 | # ------------------------------------------------------------------------------ 144 | MEDIA_URL = f"https://{aws_s3_domain}/files/" 145 | 146 | # setup email backend via Anymail 147 | # ------------------------------------------------------------------------------ 148 | # https://anymail.readthedocs.io/en/stable/installation/#installing-anymail 149 | INSTALLED_APPS += ["anymail"] # noqa F405 150 | # https://docs.djangoproject.com/en/5.0/ref/settings/#email-backend 151 | # https://anymail.readthedocs.io/en/stable/installation/#anymail-settings-reference 152 | # https://anymail.readthedocs.io/en/stable/esps/sendgrid/ 153 | EMAIL_BACKEND = "anymail.backends.sendgrid.EmailBackend" 154 | ANYMAIL = { 155 | "SENDGRID_API_KEY": env("SENDGRID_API_KEY"), # noqa F405 156 | "SENDGRID_GENERATE_MESSAGE_ID": env("SENDGRID_GENERATE_MESSAGE_ID"), # noqa F405 157 | "SENDGRID_MERGE_FIELD_FORMAT": env("SENDGRID_MERGE_FIELD_FORMAT"), # noqa F405 158 | "SENDGRID_API_URL": env("SENDGRID_API_URL", default="https://api.sendgrid.com/v3/"), # noqa F405 159 | } 160 | 161 | if len(getaddresses([env("EMAIL_RECIPIENTS")])) == 1: # noqa F405 162 | ADMINS.append(getaddresses([env("EMAIL_RECIPIENTS")])[0]) # noqa F405 # noqa F405 163 | else: 164 | recipients = getaddresses([env("EMAIL_RECIPIENTS")]) # noqa F405 165 | ADMINS += recipients # noqa F405 166 | 167 | LIST_OF_EMAIL_RECIPIENTS += list(map(lambda recipient: formataddr(recipient), ADMINS)) # noqa F405 168 | email_address = getaddresses([env("DEFAULT_FROM_EMAIL")])[0] # noqa F405 169 | DEFAULT_FROM_EMAIL = formataddr(email_address) 170 | 171 | # https://docs.djangoproject.com/en/5.0/ref/settings/#server-email 172 | SERVER_EMAIL = env("DJANGO_SERVER_EMAIL", default=DEFAULT_FROM_EMAIL) # noqa F405 173 | 174 | # https://docs.djangoproject.com/en/5.0/ref/settings/#email-subject-prefix 175 | EMAIL_SUBJECT_PREFIX = env( # noqa F405 176 | "DJANGO_EMAIL_SUBJECT_PREFIX", 177 | default="[{{ cookiecutter.project_name }}]", 178 | ) 179 | 180 | # LOGGING 181 | # ------------------------------------------------------------------------------ 182 | # https://docs.djangoproject.com/en/5.0/ref/settings/#logging 183 | # See https://docs.djangoproject.com/en/5.0/topics/logging for 184 | # more details on how to customize your logging configuration. 185 | 186 | LOGGING = { 187 | "version": 1, 188 | "disable_existing_loggers": True, 189 | "formatters": { 190 | "verbose": {"format": "%(levelname)s %(asctime)s %(module)s " "%(process)d %(thread)d %(message)s"}, 191 | # "rq_console": { 192 | # "format": "%(asctime)s %(message)s", 193 | # "datefmt": "%H:%M:%S", 194 | # }, 195 | }, 196 | "handlers": { 197 | "console": { 198 | "level": "DEBUG", 199 | "class": "logging.StreamHandler", 200 | "formatter": "verbose", 201 | }, 202 | # "rq_console": { 203 | # "level": "DEBUG", 204 | # "class": "rq.logutils.ColorizingStreamHandler", 205 | # "formatter": "rq_console", 206 | # "exclude": ["%(asctime)s"], 207 | # }, 208 | }, 209 | "root": {"level": "INFO", "handlers": ["console"]}, 210 | "loggers": { 211 | "django.db.backends": { 212 | "level": "ERROR", 213 | "handlers": ["console"], 214 | "propagate": False, 215 | }, 216 | # Errors logged by the SDK itself 217 | "sentry_sdk": {"level": "ERROR", "handlers": ["console"], "propagate": False}, 218 | "django.security.DisallowedHost": { 219 | "level": "ERROR", 220 | "handlers": ["console"], 221 | "propagate": False, 222 | }, 223 | # "rq.worker": {"handlers": ["rq_console"], "level": "DEBUG"}, 224 | }, 225 | } 226 | 227 | # Sentry 228 | # ------------------------------------------------------------------------------ 229 | SENTRY_DSN = env("SENTRY_DSN") # noqa F405 230 | SENTRY_LOG_LEVEL = env.int("DJANGO_SENTRY_LOG_LEVEL", logging.INFO) # noqa F405 231 | 232 | sentry_logging = LoggingIntegration( 233 | level=SENTRY_LOG_LEVEL, # Capture info and above as breadcrumbs 234 | event_level=logging.ERROR, # Send errors as events 235 | ) 236 | integrations = [ 237 | sentry_logging, 238 | DjangoIntegration(), 239 | RedisIntegration(), 240 | ] 241 | sentry_sdk.init( 242 | dsn=SENTRY_DSN, 243 | integrations=integrations, 244 | environment=env("SENTRY_ENVIRONMENT", default="production"), # noqa F405 245 | traces_sample_rate=env.float("SENTRY_TRACES_SAMPLE_RATE", default=0.0), # noqa F405 246 | # If you wish to associate users to errors (assuming you are using 247 | # django.contrib.auth) you may enable sending PII data. 248 | send_default_pii=True, 249 | ) 250 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/settings/test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test settings 3 | 4 | - Used to run tests fast on the continuous integration server and locally 5 | """ 6 | 7 | from email.utils import formataddr 8 | 9 | from .base import * # noqa 10 | 11 | # DEBUG 12 | # ------------------------------------------------------------------------------ 13 | # Turn debug off so tests run faster 14 | DEBUG = False 15 | 16 | TEMPLATES[0]["OPTIONS"]["debug"] = False # noqa: F405 17 | 18 | # SECRET CONFIGURATION 19 | # ------------------------------------------------------------------------------ 20 | # See: https://docs.djangoproject.com/en/5.0/ref/settings/#secret-key 21 | # Note: This key only used for development and testing. 22 | SECRET_KEY = env("DJANGO_SECRET_KEY") # noqa: F405 23 | 24 | # Mail settings 25 | # ------------------------------------------------------------------------------ 26 | EMAIL_HOST = "localhost" 27 | EMAIL_PORT = 1025 28 | 29 | # In-memory email backend stores messages in django.core.mail.outbox 30 | # for unit testing purposes 31 | EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" 32 | 33 | ADMINS.append(("Admin", "admin@example.com")) # noqa: F405 34 | LIST_OF_EMAIL_RECIPIENTS += list(map(lambda recipient: formataddr(recipient), ADMINS)) # noqa F405 35 | 36 | # CACHING 37 | # ------------------------------------------------------------------------------ 38 | # Speed advantages of in-memory caching without having to run Memcached 39 | CACHES = { 40 | "default": { 41 | "BACKEND": "django.core.cache.backends.locmem.LocMemCache", 42 | "LOCATION": "", 43 | } 44 | } 45 | 46 | # TESTING 47 | # ------------------------------------------------------------------------------ 48 | TEST_RUNNER = "django.test.runner.DiscoverRunner" 49 | 50 | # PASSWORD HASHING 51 | # ------------------------------------------------------------------------------ 52 | # Use fast password hasher so tests run faster 53 | PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"] 54 | 55 | # See https://stackoverflow.com/questions/44160666/ 56 | STORAGES = { 57 | "default": { 58 | "BACKEND": "django.core.files.storage.FileSystemStorage", 59 | }, 60 | "staticfiles": { 61 | "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", 62 | }, 63 | } 64 | 65 | # OTHER 66 | # ------------------------------------------------------------------------------ 67 | ALLOWED_HOSTS = ["example.com"] 68 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/400.html: -------------------------------------------------------------------------------- 1 | {% raw %} 2 | {% comment %} 3 | * H031 = Consider adding meta keywords. 4 | {% endcomment %} 5 | {# djlint:off H031 #} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | We’ve got some trouble | 400 - Bad Request 15 | 16 | 17 | {# djlint:on #} 18 | 19 |
20 |

21 | Bad Request 400 22 |

23 |

The server cannot process the request due to something that is perceived to be a client error.

24 |
25 | 26 | {% endraw %} 27 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/403.html: -------------------------------------------------------------------------------- 1 | {% raw %} 2 | {% comment %} 3 | * H031 = Consider adding meta keywords. 4 | {% endcomment %} 5 | {# djlint:off H031 #} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | We’ve got some trouble | 403 - Access Denied 15 | 16 | 17 | {# djlint:on #} 18 | 19 |
20 |

21 | Access Denied 403 22 |

23 |

The requested resource requires an authentication.

24 |
25 | 26 | {% endraw %} 27 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/404.html: -------------------------------------------------------------------------------- 1 | {% raw %} 2 | {% comment %} 3 | * H031 = Consider adding meta keywords. 4 | {% endcomment %} 5 | {# djlint:off H031 #} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | We’ve got some trouble | 404 - Resource not found 15 | 16 | 17 | {# djlint:on #} 18 | 19 |
20 |

21 | Resource not found 404 22 |

23 |

The requested resource could not be found but may be available again in the future.

24 |
25 | 26 | {% endraw %} 27 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/500.html: -------------------------------------------------------------------------------- 1 | {% raw %} 2 | {% comment %} 3 | * H031 = Consider adding meta keywords. 4 | {% endcomment %} 5 | {# djlint:off H031 #} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | We’ve got some trouble | 500 - Service currently unavailable 15 | 16 | 17 | {# djlint:on #} 18 | 19 |
20 |

21 | Service currently unavailable 500 22 |

23 |

24 | An unexpected condition was encountered. 25 |
26 | Our service team has been dispatched to bring it back online. 27 |

28 |
29 | 30 | {% endraw %} 31 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/base.html: -------------------------------------------------------------------------------- 1 | {% raw %}{% load static wagtailuserbar %} 2 | {% load wagtailcore_tags wagtailimages_tags %} 3 | 4 | {% comment "We ignore these rules here as they are handled in child templates" %} 5 | * H030 = Consider adding a meta description. 6 | * H031 = Consider adding meta keywords. 7 | {% endcomment %} 8 | {# djlint:off H030,H031 #} 9 | 10 | 11 | {# You can add your analytics here #} 12 | 13 | {% block title %} 14 | {% block title_prefix %}{% endraw %} 15 | {{ cookiecutter.project_name }} »{% raw %} 16 | {% endblock title_prefix %} 17 | {% if page.seo_title %} 18 | {{ page.seo_title }} 19 | {% else %} 20 | {{ page.title }} 21 | {% endif %} 22 | {% block title_suffix %} 23 | {% endblock title_suffix %} 24 | {% endblock title %} 25 | 26 | 27 | 28 | 29 | 31 | {% if request.in_preview_panel %}{% endif %} 32 | {% block extrameta %} 33 | {% endblock extrameta %} 34 | {# Favicons, generated by http://favicon.io/ #} 35 | 38 | 42 | 46 | 47 | {# Vendor stylesheets can be added here #} 48 | {# Main Stylesheet #} 49 | 50 | {% block extra_css %} 51 | {# Override this in templates to add extra stylesheets #} 52 | {% endblock extra_css %} 53 | {% load humanize %} 54 | 55 | {# djlint:on #} 56 | 57 | {% wagtailuserbar %} 58 | {% if messages %} 59 |
60 | {% for message in messages %} 61 |
62 |
63 |
64 | {{ message }} 65 | 69 |
70 |
71 |
72 | {% endfor %} 73 |
74 | {% endif %} 75 |
76 | {% include "includes/navbar.html" %} 77 | {% block promo_block %} 78 | {% endblock promo_block %} 79 |
80 | {% block content %} 81 | {% endblock content %} 82 | {% include "includes/footer.html" %} 83 | {# Bootstrap Core JavaScript #} 84 | 85 | {# Vendor scripts can be added here #} 86 | {# Main Script #} 87 | 88 | {% block extra_js %} 89 | {% endblock extra_js %} 90 | 91 | {% endraw %} 92 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/includes/footer.html: -------------------------------------------------------------------------------- 1 | {% raw %}
2 |
3 | © {% now "Y" %} {% endraw %}{{ cookiecutter.project_name }}{% raw %} 4 |
5 |
{% endraw %} 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/includes/navbar.html: -------------------------------------------------------------------------------- 1 | {% raw %}{% load static wagtailcore_tags %} 2 | {% endraw %} 44 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/wagtailadmin/home.html: -------------------------------------------------------------------------------- 1 | {% raw %}{% extends "wagtailadmin/home.html" %} 2 | {% block branding_welcome %} 3 | Welcome to {% endraw %} {{cookiecutter.project_name}} 4 | {% raw %}{% endblock branding_welcome %}{% endraw %} 5 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/templates/wagtailadmin/login.html: -------------------------------------------------------------------------------- 1 | {% raw %}{% extends "wagtailadmin/login.html" %} 2 | {% block branding_login %} 3 | Sign in to{% endraw %} {{cookiecutter.project_name}} 4 | {% raw %}{% endblock branding_login %}{% endraw %} 5 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.contrib import admin 3 | from django.urls import include, path, re_path 4 | from wagtail import urls as wagtail_urls 5 | from wagtail.admin import urls as wagtailadmin_urls 6 | from wagtail.contrib.sitemaps.views import sitemap 7 | from wagtail.documents import urls as wagtaildocs_urls 8 | 9 | from {{cookiecutter.project_slug}}.core.views import site_search 10 | 11 | urlpatterns = [ 12 | path(settings.ADMIN_URL, admin.site.urls), 13 | path("admin/", include(wagtailadmin_urls)), 14 | path("documents/", include(wagtaildocs_urls)), 15 | path("search/", site_search, name="search"), 16 | re_path(r"^sitemap\.xml$", sitemap), 17 | # Django-RQ 18 | # path("dj-rq/", include("django_rq.urls")), 19 | ] 20 | 21 | 22 | if settings.DEBUG: 23 | import debug_toolbar 24 | from django.conf.urls.static import static 25 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 26 | 27 | urlpatterns = [ 28 | path("__debug__/", include(debug_toolbar.urls)), 29 | # For django versions before 2.0: 30 | # url(r'^__debug__/', include(debug_toolbar.urls)), 31 | ] + urlpatterns 32 | 33 | # Serve static and media files from development server 34 | urlpatterns += staticfiles_urlpatterns() 35 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 36 | 37 | urlpatterns = urlpatterns + [ 38 | # For anything not caught by a more specific rule above, hand over to 39 | # Wagtail's page serving mechanism. This should be the last pattern in 40 | # the list: 41 | path("", include(wagtail_urls)), 42 | # Alternatively, if you want Wagtail pages to be served from a subpath 43 | # of your site, rather than the site root: 44 | # path("pages/", include(wagtail_urls)), 45 | ] 46 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engineervix/cookiecutter-wagtail-vix/66d5ca64237bd487092b2a65986a9254a693a008/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin 3 | 4 | from .models import User 5 | 6 | admin.site.register(User, UserAdmin) 7 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UsersConfig(AppConfig): 5 | name = "{{cookiecutter.project_slug}}.users" 6 | label = "users" 7 | verbose_name = "Users" 8 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/factories.py: -------------------------------------------------------------------------------- 1 | import factory 2 | from django.contrib.auth import get_user_model 3 | from django.contrib.auth.hashers import make_password 4 | 5 | 6 | class UserFactory(factory.django.DjangoModelFactory): 7 | """Factory for creating Django User objects""" 8 | 9 | username = factory.Faker("user_name") 10 | email = factory.Faker("email") 11 | password = factory.LazyFunction(lambda: make_password("password")) 12 | first_name = factory.Faker("first_name") 13 | last_name = factory.Faker("last_name") 14 | 15 | class Meta: 16 | model = get_user_model() 17 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.0 on 2023-12-26 01:54 2 | 3 | import django.contrib.auth.models 4 | import django.contrib.auth.validators 5 | import django.utils.timezone 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | initial = True 11 | 12 | dependencies = [ 13 | ("auth", "0012_alter_user_first_name_max_length"), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name="User", 19 | fields=[ 20 | ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), 21 | ("password", models.CharField(max_length=128, verbose_name="password")), 22 | ("last_login", models.DateTimeField(blank=True, null=True, verbose_name="last login")), 23 | ( 24 | "is_superuser", 25 | models.BooleanField( 26 | default=False, 27 | help_text="Designates that this user has all permissions without explicitly assigning them.", 28 | verbose_name="superuser status", 29 | ), 30 | ), 31 | ( 32 | "username", 33 | models.CharField( 34 | error_messages={"unique": "A user with that username already exists."}, 35 | help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", 36 | max_length=150, 37 | unique=True, 38 | validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], 39 | verbose_name="username", 40 | ), 41 | ), 42 | ("first_name", models.CharField(blank=True, max_length=150, verbose_name="first name")), 43 | ("last_name", models.CharField(blank=True, max_length=150, verbose_name="last name")), 44 | ("email", models.EmailField(blank=True, max_length=254, verbose_name="email address")), 45 | ( 46 | "is_staff", 47 | models.BooleanField( 48 | default=False, 49 | help_text="Designates whether the user can log into this admin site.", 50 | verbose_name="staff status", 51 | ), 52 | ), 53 | ( 54 | "is_active", 55 | models.BooleanField( 56 | default=True, 57 | help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", 58 | verbose_name="active", 59 | ), 60 | ), 61 | ("date_joined", models.DateTimeField(default=django.utils.timezone.now, verbose_name="date joined")), 62 | ( 63 | "groups", 64 | models.ManyToManyField( 65 | blank=True, 66 | help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", 67 | related_name="user_set", 68 | related_query_name="user", 69 | to="auth.group", 70 | verbose_name="groups", 71 | ), 72 | ), 73 | ( 74 | "user_permissions", 75 | models.ManyToManyField( 76 | blank=True, 77 | help_text="Specific permissions for this user.", 78 | related_name="user_set", 79 | related_query_name="user", 80 | to="auth.permission", 81 | verbose_name="user permissions", 82 | ), 83 | ), 84 | ], 85 | options={ 86 | "verbose_name": "user", 87 | "verbose_name_plural": "users", 88 | "abstract": False, 89 | }, 90 | managers=[ 91 | ("objects", django.contrib.auth.models.UserManager()), 92 | ], 93 | ), 94 | ] 95 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engineervix/cookiecutter-wagtail-vix/66d5ca64237bd487092b2a65986a9254a693a008/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/migrations/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractUser 2 | 3 | 4 | class User(AbstractUser): 5 | pass 6 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engineervix/cookiecutter-wagtail-vix/66d5ca64237bd487092b2a65986a9254a693a008/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/__init__.py -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/users/tests/test_factories.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from {{cookiecutter.project_slug}}.users.factories import UserFactory 4 | from {{cookiecutter.project_slug}}.users.models import User 5 | 6 | 7 | class UserFactoryTestCase(TestCase): 8 | def test_create(self): 9 | assert User.objects.count() == 0 10 | 11 | user = UserFactory() 12 | 13 | assert isinstance(user, User) 14 | assert User.objects.count() == 1 15 | 16 | self.assertTrue(user.username) 17 | self.assertTrue(user.email) 18 | self.assertTrue(user.password) 19 | self.assertTrue(user.first_name) 20 | self.assertTrue(user.last_name) 21 | -------------------------------------------------------------------------------- /{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for {{cookiecutter.project_slug}} project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | if os.getenv("WEB_CONCURRENCY"): # Feel free to change this condition 15 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{cookiecutter.project_slug}}.settings.production") 16 | else: 17 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{cookiecutter.project_slug}}.settings.dev") 18 | 19 | 20 | application = get_wsgi_application() 21 | --------------------------------------------------------------------------------