├── template
├── [[project_name]]
│ ├── __init__.py
│ ├── home
│ │ ├── __init__.py
│ │ ├── admin.py
│ │ ├── signals.py
│ │ ├── management
│ │ │ ├── __init__.py
│ │ │ └── commands
│ │ │ │ └── __init__.py
│ │ ├── migrations
│ │ │ └── __init__.py
│ │ ├── apps.py.jinja
│ │ ├── models.py
│ │ ├── templatetags
│ │ │ └── replace.py
│ │ ├── tests.py
│ │ ├── urls.py
│ │ ├── views.py
│ │ └── forms.py.jinja
│ ├── user
│ │ ├── __init__.py
│ │ ├── migrations
│ │ │ └── __init__.py
│ │ ├── apps.py.jinja
│ │ ├── urls.py
│ │ ├── baker_recipes.py
│ │ ├── views.py
│ │ ├── adapter.py
│ │ ├── forms.py
│ │ └── admin.py
│ ├── util
│ │ ├── __init__.py
│ │ ├── migrations
│ │ │ ├── __init__.py
│ │ │ └── 0001_initial.py.jinja
│ │ ├── apps.py.jinja
│ │ ├── widgets.py
│ │ ├── models.py
│ │ ├── util.py
│ │ ├── middleware.py
│ │ └── tests.py
│ ├── config
│ │ ├── __init__.py
│ │ ├── settings
│ │ │ ├── __init__.py
│ │ │ ├── build.py
│ │ │ └── test.py
│ │ ├── websocket.py
│ │ ├── asgi.py.jinja
│ │ ├── wsgi.py.jinja
│ │ └── urls.py.jinja
│ ├── components
│ │ ├── __init__.py
│ │ ├── link
│ │ │ ├── link.html
│ │ │ └── link.py
│ │ ├── form
│ │ │ ├── label.html
│ │ │ ├── widgets
│ │ │ │ ├── date.html
│ │ │ │ ├── datetime.html
│ │ │ │ ├── select.html
│ │ │ │ ├── input.html
│ │ │ │ ├── textarea.html
│ │ │ │ ├── checkbox_select.html
│ │ │ │ ├── password.html
│ │ │ │ └── radio.html
│ │ │ ├── form.html
│ │ │ ├── flatpickr.html
│ │ │ ├── radio.html
│ │ │ ├── checkbox.html
│ │ │ ├── input.html
│ │ │ └── toggle.html
│ │ ├── footer
│ │ │ ├── footer_nav_link.html
│ │ │ ├── social_link.html
│ │ │ └── footer.py
│ │ ├── modal
│ │ │ ├── modal.py
│ │ │ └── modal.html
│ │ ├── svg
│ │ │ ├── svg.py
│ │ │ ├── facebook.svg
│ │ │ ├── alpine.svg
│ │ │ ├── twitter.svg
│ │ │ ├── github.svg
│ │ │ ├── dribbble.svg
│ │ │ ├── apple.svg
│ │ │ ├── django.svg
│ │ │ ├── google.svg
│ │ │ ├── linkedin.svg
│ │ │ ├── instagram.svg
│ │ │ ├── discord.svg
│ │ │ └── slack.svg
│ │ ├── button
│ │ │ ├── button.html
│ │ │ └── button.py
│ │ ├── popover
│ │ │ ├── popover.html
│ │ │ └── popover.py
│ │ ├── tabs
│ │ │ └── tabs.html
│ │ ├── header
│ │ │ ├── header.py
│ │ │ └── profile-popover.html
│ │ └── alert
│ │ │ ├── alert.html
│ │ │ └── alert.py
│ ├── static_source
│ │ ├── .keep
│ │ ├── js
│ │ │ ├── index.ts
│ │ │ ├── alpinejs__ui.d.ts
│ │ │ ├── components.ts
│ │ │ ├── global.d.ts
│ │ │ ├── test
│ │ │ │ ├── main.test.ts
│ │ │ │ └── links.test.ts
│ │ │ ├── forms
│ │ │ │ ├── select.js
│ │ │ │ ├── date_datetime.ts
│ │ │ │ ├── input.ts
│ │ │ │ └── common.ts
│ │ │ ├── links.ts
│ │ │ └── main.ts.jinja
│ │ ├── css
│ │ │ ├── styles.js
│ │ │ ├── admin.js
│ │ │ ├── _tailwind.scss
│ │ │ ├── components
│ │ │ │ ├── _index.scss
│ │ │ │ ├── _messages.scss
│ │ │ │ ├── _links.scss
│ │ │ │ ├── _alert.scss
│ │ │ │ └── _buttons.scss
│ │ │ ├── admin.scss
│ │ │ ├── site.scss
│ │ │ └── base
│ │ │ │ ├── _typography.scss
│ │ │ │ ├── _fonts.scss
│ │ │ │ ├── _colors.scss
│ │ │ │ ├── _index.scss
│ │ │ │ └── _flatpickr.scss
│ │ └── img
│ │ │ ├── favicons
│ │ │ ├── favicon.ico
│ │ │ └── favicon.svg
│ │ │ ├── logo.svg
│ │ │ ├── logomark.svg
│ │ │ └── loading.svg
│ ├── templates
│ │ ├── django
│ │ │ └── forms
│ │ │ │ └── readme.txt
│ │ ├── account
│ │ │ ├── snippets
│ │ │ │ ├── already_logged_in.html
│ │ │ │ └── social_login_buttons.html
│ │ │ ├── password_reset_from_key_done.html
│ │ │ ├── account_inactive.html
│ │ │ ├── logout.html
│ │ │ ├── password_reset_done.html
│ │ │ ├── account_base.html
│ │ │ ├── password_reset_from_key.html
│ │ │ ├── password_reset.html
│ │ │ ├── signup.html
│ │ │ └── login.html
│ │ ├── messages.html
│ │ ├── samples
│ │ │ └── current_time.html
│ │ ├── header
│ │ │ ├── header_link.html
│ │ │ ├── desktop_center.html
│ │ │ ├── logo.html
│ │ │ ├── profile_menu_item.html
│ │ │ ├── base.html
│ │ │ ├── mobile_menu_button.html
│ │ │ ├── end.html
│ │ │ └── mobile_menu.html
│ │ ├── 404.html
│ │ ├── 400.html
│ │ ├── 403.html
│ │ ├── home
│ │ │ └── form_test.html
│ │ ├── 500.html
│ │ ├── admin
│ │ │ └── base_site.html.jinja
│ │ ├── components
│ │ │ └── open_graph_tags.html
│ │ ├── footer.html.jinja
│ │ ├── base.html
│ │ └── index.html
│ └── tests.py
├── [[ _copier_conf.answers_file ]].jinja
├── .ignore
├── vitest.config.ts
├── .dockerignore.jinja
├── postcss.config.js
├── mise.ci.toml
├── eslint.config.mjs
├── build.sh
├── .vscode
│ ├── extensions.json
│ ├── settings.json
│ ├── tasks.json
│ └── launch.json.jinja
├── .watchmanconfig.jinja
├── gunicorn.conf.py.jinja
├── mise-tasks
│ ├── db
│ │ ├── check
│ │ ├── create-user
│ │ └── setup
│ └── utils
│ │ └── colors
├── .editorconfig
├── scripts
│ ├── pull_remote_db.sh.jinja
│ ├── create_patch.sh.jinja
│ └── export_project.py
├── .github
│ ├── workflows
│ │ ├── fly-deploy.yml
│ │ └── django_ci.yml.jinja
│ ├── pull_request_template.md
│ └── dependabot.yml
├── tsconfig.json.jinja
├── fly.toml.jinja
├── .stylelintrc
├── manage.py.jinja
├── Dockerfile.jinja
├── package.json.jinja
├── .pre-commit-config.yaml.jinja
├── conftest.py
├── vite.config.mjs.jinja
├── tailwind.config.js.jinja
└── render.yaml.jinja
├── django_hydra.py
├── .flake8
├── .readthedocs.yaml
├── .editorconfig
├── pyproject.toml
├── docs
├── Makefile
├── make.bat
└── source
│ ├── conf.py
│ ├── index.rst
│ ├── contributing.rst
│ ├── debugging.rst
│ └── testing.rst
├── .github
└── dependabot.yml
├── test.sh
├── .vscode
└── settings.json
├── copier.yml
├── todo.txt
├── scripts
├── mac_intel_install.sh
└── export_project.py
└── .gitignore
/template/[[project_name]]/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/template/[[project_name]]/home/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/template/[[project_name]]/home/admin.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/template/[[project_name]]/home/signals.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/template/[[project_name]]/user/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/template/[[project_name]]/util/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/template/[[project_name]]/config/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/template/[[project_name]]/config/settings/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/template/[[project_name]]/home/management/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/template/[[project_name]]/home/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/template/[[project_name]]/static_source/.keep:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/template/[[project_name]]/static_source/js/index.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/template/[[project_name]]/user/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/template/[[project_name]]/util/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/template/[[project_name]]/home/management/commands/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django_hydra.py:
--------------------------------------------------------------------------------
1 | # This exists purely so `poetry build` will work
2 |
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | ignore = E402
3 | exclude =
4 | max-complexity = 10
--------------------------------------------------------------------------------
/template/[[project_name]]/static_source/js/alpinejs__ui.d.ts:
--------------------------------------------------------------------------------
1 | declare module "@alpinejs/ui";
2 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/link/link.html:
--------------------------------------------------------------------------------
1 | {{ text }}
2 |
--------------------------------------------------------------------------------
/template/[[project_name]]/static_source/css/styles.js:
--------------------------------------------------------------------------------
1 | // vite will only build js files
2 | import "@/css/site.scss";
3 |
--------------------------------------------------------------------------------
/template/[[project_name]]/static_source/css/admin.js:
--------------------------------------------------------------------------------
1 | // styles for the django admin interface
2 | import "@/css/admin.scss";
3 |
--------------------------------------------------------------------------------
/template/[[project_name]]/templates/django/forms/readme.txt:
--------------------------------------------------------------------------------
1 | These files are just to overwrite the default forms in django
2 |
--------------------------------------------------------------------------------
/template/[[project_name]]/static_source/css/_tailwind.scss:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/template/[[ _copier_conf.answers_file ]].jinja:
--------------------------------------------------------------------------------
1 | # Changes here will be overwritten by Copier; NEVER EDIT MANUALLY
2 | [[ _copier_answers|to_nice_yaml -]]
3 |
--------------------------------------------------------------------------------
/template/[[project_name]]/templates/account/snippets/already_logged_in.html:
--------------------------------------------------------------------------------
1 |
2 | {% include "header/header_link.html" with link="" text="Team" %}
3 | {% include "header/header_link.html" with link="" text="Products" %}
4 | {% include "header/header_link.html" with link="" text="Calendar" %}
5 |
6 |
--------------------------------------------------------------------------------
/template/[[project_name]]/templates/account/password_reset_from_key_done.html:
--------------------------------------------------------------------------------
1 | {% extends "account/account_base.html" %}
2 | {% block account_title %}
3 | {% translate "Change Password" %}
4 | {% endblock account_title %}
5 | {% block account_content %}
6 |
3 | {{ errors }}
4 | {% csrf_token %}
5 | {% for field, errors in fields %}
6 | {% component "field" field=field / %}
7 | {% endfor %}
8 |
9 | {% for field in hidden_fields %}
10 | {% component "field" field=field / %}
11 | {% endfor %}
12 |
13 |
14 | {% endprovide %}
15 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 |
8 | [*]
9 | end_of_line = lf
10 | charset = utf-8
11 | trim_trailing_whitespace = true
12 | insert_final_newline = true
13 | indent_style = space
14 | indent_size = 4
15 |
16 | [*.{js,html,css,yml,xml,scss,json}]
17 | indent_style = space
18 | indent_size = 2
19 |
20 | [*.md]
21 | trim_trailing_whitespace = false
22 |
--------------------------------------------------------------------------------
/template/[[project_name]]/templates/header/logo.html:
--------------------------------------------------------------------------------
1 | "]
6 |
7 | [tool.poetry.dependencies]
8 | python = "^3.11"
9 |
10 | [tool.poetry.group.dev.dependencies]
11 | isort = "^5.13.2"
12 |
13 | [tool.poetry.group.docs.dependencies]
14 | Sphinx = "^8.1.3"
15 | furo = "^2024.7.18"
16 |
17 | [build-system]
18 | requires = ["poetry-core>=1.0.0"]
19 | build-backend = "poetry.core.masonry.api"
20 |
--------------------------------------------------------------------------------
/template/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 |
8 | [*]
9 | end_of_line = lf
10 | charset = utf-8
11 | trim_trailing_whitespace = true
12 | insert_final_newline = true
13 | indent_style = space
14 | indent_size = 4
15 |
16 | [*.{js,html,css,yml,xml,scss,json,ts,jinja}]
17 | indent_style = space
18 | indent_size = 2
19 |
20 | [*.md]
21 | trim_trailing_whitespace = false
22 |
--------------------------------------------------------------------------------
/template/[[project_name]]/static_source/css/base/_typography.scss:
--------------------------------------------------------------------------------
1 | @layer base {
2 | h1 {
3 | @apply text-4xl font-bold mb-3;
4 | }
5 |
6 | h2 {
7 | @apply text-3xl font-bold mb-2;
8 | }
9 |
10 |
11 | h3 {
12 | @apply text-3xl font-semibold mb-2;
13 | }
14 |
15 | h4 {
16 | @apply text-2xl font-semibold mb-1;
17 | }
18 |
19 | h5 {
20 | @apply text-xl mb-1;
21 | }
22 |
23 | h6 {
24 | @apply text-lg;
25 | }
26 |
27 | ul {
28 | @apply list-disc list-inside;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/template/[[project_name]]/templates/404.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block title %}
3 | {% translate "Page not found" %}
4 | {% endblock title %}
5 | {% block content %}
6 |
7 |
8 |
{% translate "Page not found" %}
9 |
{% translate "This is not the page you were looking for." %}
10 |
11 | {% if exception %}{{ exception }}{% endif %}
12 |
13 |
14 |
15 | {% endblock content %}
16 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/form/radio.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {% component "label" field=field %}
4 | {{ field.label }}
5 | {% endcomponent %}
6 |
7 | {% component "widget" field=field / %}
8 | {% if field.help_text %}
9 | {{ field.help_text|safe }}
11 | {% endif %}
12 | {% if field.errors %}{{ field.errors }}
{% endif %}
13 |
14 |
--------------------------------------------------------------------------------
/template/[[project_name]]/config/settings/build.py:
--------------------------------------------------------------------------------
1 | # required settings for build command like collectstatic
2 | import os
3 |
4 | REQUIRED_KEYS = [
5 | "SECRET_KEY",
6 | "DATABASE_URL",
7 | "DJANGO_ALLOWED_HOSTS",
8 | "REDIS_URL",
9 | "AWS_ACCESS_KEY_ID",
10 | "AWS_SECRET_ACCESS_KEY",
11 | "BUCKET_NAME",
12 | "AWS_ENDPOINT_URL_S3",
13 | ]
14 | for key in REQUIRED_KEYS:
15 | os.environ[key] = "dummy"
16 | os.environ["SENTRY_DSN"] = "https://dummy@dummy.ingest.sentry.io/1234567"
17 |
18 | from .prod import *
19 |
--------------------------------------------------------------------------------
/template/[[project_name]]/config/websocket.py:
--------------------------------------------------------------------------------
1 | async def websocket_application(scope, receive, send):
2 | # pylint: disable=unused-argument
3 | while True:
4 | event = await receive()
5 |
6 | if event["type"] == "websocket.connect":
7 | await send({"type": "websocket.accept"})
8 |
9 | if event["type"] == "websocket.disconnect":
10 | break
11 |
12 | if event["type"] == "websocket.receive":
13 | if event["text"] == "ping":
14 | await send({"type": "websocket.send", "text": "pong!"})
15 |
--------------------------------------------------------------------------------
/template/scripts/pull_remote_db.sh.jinja:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 |
4 | export ENV_NAME="[[project_name]]"
5 |
6 | dropdb $ENV_NAME
7 | heroku pg:pull DATABASE_URL $ENV_NAME --app $ENV_NAME-development
8 | for tbl in `psql -qAt -c "select tablename from pg_tables where schemaname = 'public';" $ENV_NAME` ; do psql -c "alter table \"$tbl\" owner to $ENV_NAME" $ENV_NAME ; done
9 | for tbl in `psql -qAt -c "select sequence_name from information_schema.sequences where sequence_schema = 'public';" $ENV_NAME` ; do psql -c "alter table \"$tbl\" owner to $ENV_NAME" $ENV_NAME ; done
10 |
--------------------------------------------------------------------------------
/template/[[project_name]]/templates/400.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block title %}
3 | {% translate "Bad Request (400)" %}
4 | {% endblock title %}
5 | {% block content %}
6 |
7 |
8 |
{% translate "Bad Request (400)" %}
9 |
10 | {% if exception %}
11 | {{ exception }}
12 | {% else %}
13 | {% translate "This request was a bad, bad request." %}
14 | {% endif %}
15 |
16 |
17 |
18 | {% endblock content %}
19 |
--------------------------------------------------------------------------------
/template/[[project_name]]/templates/403.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block title %}
3 | {% translate "Forbidden (403)" %}
4 | {% endblock title %}
5 | {% block content %}
6 |
7 |
8 |
{% translate "Forbidden (403)" %}
9 |
10 | {% if exception %}
11 | {{ exception }}
12 | {% else %}
13 | {% translate "You're not allowed to access this page." %}
14 | {% endif %}
15 |
16 |
17 |
18 | {% endblock content %}
19 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/button/button.html:
--------------------------------------------------------------------------------
1 | {% if not href %}
2 |
3 | {% if is_htmx %}
4 |
9 | {% endif %}
10 | {% slot "content" default required %}
11 | {% endslot %}
12 |
13 | {% else %}
14 |
15 | {% slot "content" default required %}
16 | {% endslot %}
17 |
18 | {% endif %}
19 |
--------------------------------------------------------------------------------
/template/[[project_name]]/templates/home/form_test.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block title %}
3 | Form Test
4 | {% endblock title %}
5 | {% block content %}
6 |
7 |
Form Field Test
8 |
16 |
17 | {% endblock content %}
18 |
--------------------------------------------------------------------------------
/template/[[project_name]]/static_source/js/forms/select.js:
--------------------------------------------------------------------------------
1 | import Alpine from "alpinejs";
2 | import TomSelect from "tom-select";
3 | import "tom-select/dist/css/tom-select.bootstrap5.css"; // doesn't actually include bootstrap, easy to style
4 |
5 | const select = () => ({
6 | init() {
7 | // give a timeout to let htmx finish swapping content in
8 | // eslint-disable-next-line no-undef
9 | setTimeout(() => {
10 | const control = this.$el;
11 |
12 | new TomSelect(control, {
13 | });
14 | }, 80);
15 | },
16 | });
17 |
18 | Alpine.data("select", select);
19 |
--------------------------------------------------------------------------------
/template/.github/workflows/fly-deploy.yml:
--------------------------------------------------------------------------------
1 | # See https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/
2 |
3 | name: Fly Deploy
4 | on:
5 | push:
6 | branches:
7 | - master
8 | jobs:
9 | deploy:
10 | name: Deploy app
11 | runs-on: ubuntu-latest
12 | concurrency: deploy-group # optional: ensure only one action runs at a time
13 | steps:
14 | - uses: actions/checkout@v4
15 | - uses: superfly/flyctl-actions/setup-flyctl@master
16 | - run: flyctl deploy --remote-only
17 | env:
18 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
19 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/form/widgets/select.html:
--------------------------------------------------------------------------------
1 |
6 | {% for group_name, group_choices, group_index in widget.optgroups %}
7 | {% if group_name %}{% endif %}
8 | {% for option in group_choices %}
9 | {% include option.template_name with widget=option %}
10 | {% endfor %}
11 | {% if group_name %} {% endif %}
12 | {% endfor %}
13 |
14 |
--------------------------------------------------------------------------------
/template/[[project_name]]/templates/500.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block title %}
3 | {% translate "Server Error" %}
4 | {% endblock title %}
5 | {% block content %}
6 |
7 |
8 |
{% translate "Ooops!!! 500" %}
9 |
{% translate "Looks like something went wrong!" %}
10 |
11 | {% translate "We track these errors automatically, but if the problem persists feel free to contact us. In the meantime, try refreshing." %}
12 |
13 |
14 |
15 | {% endblock content %}
16 |
--------------------------------------------------------------------------------
/template/[[project_name]]/config/settings/test.py:
--------------------------------------------------------------------------------
1 | # pylint: disable-all
2 |
3 | from .local import * # noqa
4 |
5 | # PASSWORDS
6 | # ------------------------------------------------------------------------------
7 | # https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers
8 | PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
9 |
10 | MIDDLEWARE.remove("debug_toolbar.middleware.DebugToolbarMiddleware")
11 | INSTALLED_APPS.remove("debug_toolbar")
12 |
13 | TEMPLATE_DEBUG = False
14 | DJANGO_VITE_DEV_MODE = False
15 | DJANGO_VITE_MANIFEST_PATH = DJANGO_VITE_ASSETS_PATH / "manifest.json"
16 |
--------------------------------------------------------------------------------
/template/[[project_name]]/templates/header/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {% include "header/mobile_menu_button.html" %}
7 | {% include "header/logo.html" %}
8 | {% include "header/desktop_center.html" %}
9 |
10 |
{% include "header/end.html" %}
11 |
12 |
13 | {% include "header/mobile_menu.html" %}
14 |
15 |
16 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/form/widgets/input.html:
--------------------------------------------------------------------------------
1 | {% load replace %}
2 |
12 |
--------------------------------------------------------------------------------
/template/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | # Short description
2 |
3 | This pull request...
4 |
5 | # Any point in the PR you think needs extra attention
6 |
7 | N/A
8 |
9 | # New Features or Fixes
10 |
11 | 1.
12 |
13 | # Video of feature working
14 |
15 |
16 |
17 | # Pull Request Checklist
18 |
19 | - [ ] Where relevant, I've added new tests and docs
20 | - [ ] My branch is pulled off of the latest develop
21 | - [ ] I've used a rebase strategy for any commits I made
22 | - [ ] I've double checked my code works in chrome & 1 other browser
23 | - [ ] I've double checked the design & ticket requirements
24 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/svg/facebook.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 | Facebook
7 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/template/[[project_name]]/util/util.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import re
3 | import time
4 |
5 | from django.utils.deconstruct import deconstructible
6 |
7 |
8 | @deconstructible
9 | class file_url: # NOQA
10 | path = "uploads/{0}/{1.year:04}/{1.month:02}/{1.day:02}/{2}/{3}" # NOQA
11 |
12 | def __init__(self, category):
13 | self.category = category
14 |
15 | def __call__(self, instance, filename):
16 | r = re.compile(r"[^\S]")
17 | filename = r.sub("", filename)
18 | now = datetime.datetime.now()
19 | timestamp = int(time.time())
20 | return self.path.format(self.category, now, timestamp, filename)
21 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/form/checkbox.html:
--------------------------------------------------------------------------------
1 |
2 |
{% component "widget" field=field / %}
3 |
4 | {% if field.label %}
5 | {% component "checkbox_label" field=field %}
6 | {{ field.label }}
7 | {% endcomponent %}
8 | {% endif %}
9 | {% if field.help_text %}
10 |
{{ field.help_text|safe }}
12 | {% endif %}
13 | {% if field.errors %}
{{ field.errors }}
{% endif %}
14 |
15 |
16 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/popover/popover.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {% slot "trigger" %}
4 | {% component "button" %}
5 | open menu
6 | {% endcomponent %}
7 | {% endslot %}
8 |
9 |
13 | {% slot "content" %}
14 | Content goes here
15 | {% endslot %}
16 |
17 |
18 |
--------------------------------------------------------------------------------
/template/[[project_name]]/home/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from django.views.generic.base import TemplateView
3 |
4 | from .views import FormTestView, current_time, error, test_message_redirect, test_message_refresh
5 |
6 | urlpatterns = [
7 | path("form-test/", FormTestView.as_view(), name="form_test"),
8 | path("current-time/", current_time, name="current_time"),
9 | path("test-refresh/", test_message_refresh, name="test_refresh"),
10 | path("test-redirect/", test_message_redirect, name="test_redirect"),
11 | path("error/", error, name="error"),
12 | path("", TemplateView.as_view(template_name="index.html"), name="home"),
13 | ]
14 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/svg/alpine.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 | Alpine
8 |
9 |
11 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/template/[[project_name]]/templates/admin/base_site.html.jinja:
--------------------------------------------------------------------------------
1 | {% extends "admin/base_site.html" %}
2 | {% load django_vite %}
3 | {% block extrahead %}
4 | {{ block.super }}
5 | {% vite_hmr_client %}
6 |
7 | {% endblock extrahead %}
8 | {% block title %}
9 | {% if subtitle %}{{ subtitle }} |{% endif %}
10 | {{ title }} | {{ site_title|default:_("[[project_name_verbose]] site admin") }}
11 | {% endblock title %}
12 | {% block branding %}
13 |
16 | {% endblock branding %}
17 |
--------------------------------------------------------------------------------
/template/[[project_name]]/user/views.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.mixins import LoginRequiredMixin
2 | from django.views.generic import RedirectView
3 | from django_htmx.http import HttpResponseClientRedirect
4 |
5 |
6 | class UserRedirectView(LoginRequiredMixin, RedirectView):
7 | permanent = False
8 |
9 | def get(self, request, *args, **kwargs):
10 | url = self.get_redirect_url(*args, **kwargs)
11 | if request.htmx:
12 | return HttpResponseClientRedirect(url)
13 | return super().get(request, *args, **kwargs)
14 |
15 | def get_redirect_url(self, *args, **kwargs):
16 | return "/"
17 |
18 |
19 | user_redirect_view = UserRedirectView.as_view()
20 |
--------------------------------------------------------------------------------
/template/tsconfig.json.jinja:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "types": ["vite/client"],
4 | "target": "es6",
5 | "useDefineForClassFields": true,
6 | "module": "esnext",
7 | "moduleResolution": "bundler",
8 | "strict": true,
9 | "jsx": "preserve",
10 | "sourceMap": true,
11 | "resolveJsonModule": true,
12 | "esModuleInterop": true,
13 | "skipLibCheck": true,
14 | "lib": ["esnext", "dom"]
15 | },
16 | "include": [
17 | "[[project_name]]/**/*.ts",
18 | "[[project_name]]/**/*.d.ts",
19 | "[[project_name]]/**/*.tsx",
20 | "[[project_name]]/**/*.vue",
21 | "[[project_name]]/**/*.js",
22 | "./*.js",
23 | "./*.ts"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/footer/footer.py:
--------------------------------------------------------------------------------
1 | from django_components import Component, register
2 |
3 |
4 | @register("footer_nav_link")
5 | class FooterNavLink(Component):
6 | template_name = "footer_nav_link.html"
7 |
8 | def get_context_data(self, href, text):
9 | return {"href": href, "text": text}
10 |
11 |
12 | @register("social_link")
13 | class SocialLink(Component):
14 | template_name = "social_link.html"
15 |
16 | def get_context_data(self, name, href="#", icon_type=None, size="5"):
17 | return {
18 | "href": href,
19 | "name": name,
20 | "icon_type": icon_type or name.lower(),
21 | "size": size,
22 | }
23 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = source
9 | BUILDDIR = build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/form/widgets/textarea.html:
--------------------------------------------------------------------------------
1 | {% load replace %}
2 |
7 |
13 |
14 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/svg/twitter.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 | Twitter
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/template/[[project_name]]/static_source/css/base/_fonts.scss:
--------------------------------------------------------------------------------
1 | // Example font family inclusion
2 | // @font-face {
3 | // font-family: "GT Walsheim Pro";
4 | // src: url("/static/fonts/GT-Walsheim-Pro-Regular.otf") format("opentype");
5 | // font-weight: 400;
6 | // font-style: normal;
7 | // }
8 |
9 | // @font-face {
10 | // font-family: "GT Walsheim Pro";
11 | // src: url("/static/fonts/GT-Walsheim-Pro-Medium.otf") format("opentype");
12 | // font-weight: 500;
13 | // font-style: normal;
14 | // }
15 |
16 | // @font-face {
17 | // font-family: "GT Walsheim Pro";
18 | // src: url("/static/fonts/GT-Walsheim-Pro-Bold.otf") format("opentype");
19 | // font-weight: 700;
20 | // font-style: normal;
21 | // }
22 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/tabs/tabs.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {% for tab in tabs %}
4 | {{ tab.header }}
9 | {% endfor %}
10 |
11 |
12 | {% for tab in tabs %}
13 |
14 | {{ tab.content }}
15 |
16 | {% endfor %}
17 |
18 |
19 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/popover/popover.py:
--------------------------------------------------------------------------------
1 | from django_components import Component, register
2 |
3 |
4 | @register("popover")
5 | class Popover(Component):
6 | """
7 | Base popover component using Alpine.js UI.
8 |
9 | Template slots:
10 | - trigger: Content for the button that triggers the popover
11 | - content: Content displayed in the popover panel
12 |
13 | Context variables:
14 | - wrapper_class: Classes for the wrapper div (default: 'relative')
15 | - button_class: Classes for the trigger button
16 | - panel_class: Classes for the popover panel
17 | """
18 |
19 | template_name = "popover.html"
20 |
21 | def get_context_data(self, **kwargs):
22 | return kwargs
23 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # TODO: Add `github-action` ecosystem check when PR #171 lands
2 |
3 | version: 2
4 | updates:
5 | - package-ecosystem: "pip"
6 | directory: "/"
7 | schedule:
8 | interval: "weekly"
9 | target-branch: "develop"
10 | # Limit pull requests to 1 so Dependabot isn't constantly rebasing PRs.
11 | # https://github.com/python-poetry/poetry/issues/496
12 | open-pull-requests-limit: 1
13 |
14 | - package-ecosystem: "pip"
15 | directory: "/[[project_name]]"
16 | schedule:
17 | interval: "weekly"
18 | target-branch: "develop"
19 | open-pull-requests-limit: 1
20 |
21 | - package-ecosystem: "npm"
22 | directory: "/[[project_name]]"
23 | schedule:
24 | interval: "weekly"
25 | target-branch: "develop"
26 |
--------------------------------------------------------------------------------
/template/[[project_name]]/templates/components/open_graph_tags.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/template/mise-tasks/db/create-user:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # @desc Create PostgreSQL user for current system user
3 | # @depends db:check
4 |
5 | set -euo pipefail
6 |
7 | # Source colors
8 | source "$(dirname "$0")/../utils/colors"
9 |
10 | color_info "Creating PostgreSQL user..."
11 |
12 | # Check if createuser exists
13 | if ! command -v createuser >/dev/null 2>&1; then
14 | color_error "PostgreSQL client tools not found. Please install PostgreSQL."
15 | exit 1
16 | fi
17 |
18 | color_info "Setting up PostgreSQL user..."
19 |
20 | # Try to create user, ignore if already exists
21 | createuser -sdl "$USER" 2>/dev/null || true
22 |
23 | # Try to create database, ignore if already exists
24 | createdb "$USER" 2>/dev/null || true
25 |
26 | color_success "PostgreSQL user setup complete"
27 |
--------------------------------------------------------------------------------
/template/[[project_name]]/templates/account/logout.html:
--------------------------------------------------------------------------------
1 | {% extends "account/account_base.html" %}
2 | {% block account_title %}
3 | {% translate "Sign Out" %}
4 | {% endblock account_title %}
5 | {% block account_content %}
6 | {% translate "Are you sure you want to sign out?" %}
7 |
20 | {% endblock account_content %}
21 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/form/input.html:
--------------------------------------------------------------------------------
1 | {% load heroicons %}
2 |
3 |
6 | {% component "widget" field=field / %}
7 | {% if field.label %}
8 | {% component "label" field=field %}
9 | {{ field.label }}
10 | {% endcomponent %}
11 | {% endif %}
12 |
13 | {% if field.errors %}
14 |
{% heroicon_solid "exclamation-circle" class="text-error" %}
15 |
{{ field.errors }}
16 | {% endif %}
17 | {% if field.help_text %}
18 |
{{ field.help_text|safe }}
20 | {% endif %}
21 |
22 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/header/header.py:
--------------------------------------------------------------------------------
1 | from django_components import Component, register
2 |
3 |
4 | @register("profile-popover")
5 | class ProfilePopover(Component):
6 | """
7 | Profile-specific popover component extending the base popover functionality.
8 |
9 | Template slots:
10 | - menu_items: Menu items to be displayed in the popover
11 |
12 | Context variables:
13 | - avatar_url: URL for the user's avatar image
14 | - user_name: User's display name
15 | - user_email: User's email address
16 | - show_chevron: Whether to show the dropdown chevron (default: True)
17 | - show_profile: Whether to show the profile header section (default: True)
18 | """
19 |
20 | template_name = "profile-popover.html"
21 |
22 | def get_context_data(self, **kwargs):
23 | return kwargs
24 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/link/link.py:
--------------------------------------------------------------------------------
1 | from django_components import Component, register
2 |
3 |
4 | @register("link")
5 | class Button(Component):
6 | """
7 | Link component
8 |
9 | """
10 |
11 | template_name = "link.html"
12 |
13 | def get_context_data(
14 | self,
15 | text: str,
16 | url: str,
17 | attrs: dict[str, str] | None = None,
18 | ):
19 | if attrs is None:
20 | attrs = {}
21 |
22 | # Build base classes
23 | classes = [
24 | "text-blue-500",
25 | "hover:text-blue-700",
26 | "underline",
27 | ]
28 |
29 | attrs["class"] = f"{' '.join(classes)} {attrs.get('class', '')}".strip()
30 |
31 | return {
32 | "text": text,
33 | "url": url,
34 | "attrs": attrs,
35 | }
36 |
--------------------------------------------------------------------------------
/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 | original=$(pwd)
4 | keepenv=false
5 |
6 | RED='\033[0;31m'
7 | YELLOW='\033[1;33m'
8 | CLEAR='\033[0m'
9 |
10 |
11 | tmpfolder=""
12 | appname=sampleapp
13 | appdir=../$appname
14 |
15 | unset DJANGO_SETTINGS_MODULE
16 |
17 |
18 |
19 | printf "${RED}Removing old app${CLEAR}\n"
20 | if [[ -d "../$appname" ]]
21 | then
22 | set +e
23 | rm -rf ../$appname
24 | dropdb test_sampleapp
25 | dropdb sampleapp
26 | set -e
27 | fi
28 |
29 | echo "Creating App"
30 | cookiecutter . --default-config --no-input project_name=$appname -o ../
31 |
32 |
33 | echo "Running tests"
34 | cd ../$appname/
35 |
36 | eval "$(direnv export bash)"
37 | ./scripts/create_new_project.sh
38 | npm run build
39 | pre-commit run --all-files
40 | playwright install
41 | poetry run pytest
42 |
43 |
44 | RV=$?
45 | rm -rf static/
46 | cd $original
47 | exit $RV
48 |
--------------------------------------------------------------------------------
/template/[[project_name]]/util/migrations/0001_initial.py.jinja:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.0 on 2022-01-11 20:07
2 |
3 | from django.db import migrations, models
4 |
5 | import [[project_name]].util.util
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | ]
14 |
15 | operations = [
16 | migrations.CreateModel(
17 | name='TestFileModel',
18 | fields=[
19 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20 | ('file_field', models.ImageField(upload_to=[[project_name]].util.util.file_url('filez'), verbose_name='foo')),
21 | ],
22 | options={
23 | 'verbose_name': 'test',
24 | 'verbose_name_plural': 'tests',
25 | },
26 | ),
27 | ]
28 |
--------------------------------------------------------------------------------
/template/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | #fix when uv support comes out
9 | # - package-ecosystem: "pip"
10 | # directory: "/"
11 | # schedule:
12 | # interval: "weekly"
13 | # target-branch: "develop"
14 | # # Limit pull requests to 1 so Dependabot isn't constantly rebasing PRs.
15 | # # https://github.com/python-poetry/poetry/issues/496
16 | # open-pull-requests-limit: 1
17 |
18 | - package-ecosystem: "npm"
19 | directory: "/"
20 | schedule:
21 | interval: "weekly"
22 | target-branch: "develop"
23 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/form/widgets/checkbox_select.html:
--------------------------------------------------------------------------------
1 |
2 | {% for group, options, index in widget.optgroups %}
3 | {% if group %}
{{ group }}
{% endif %}
4 | {% for option in options %}
5 |
6 |
13 | {{ option.label }}
14 |
15 | {% endfor %}
16 | {% endfor %}
17 |
18 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=source
11 | set BUILDDIR=build
12 |
13 | %SPHINXBUILD% >NUL 2>NUL
14 | if errorlevel 9009 (
15 | echo.
16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
17 | echo.installed, then set the SPHINXBUILD environment variable to point
18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
19 | echo.may add the Sphinx directory to PATH.
20 | echo.
21 | echo.If you don't have Sphinx installed, grab it from
22 | echo.https://www.sphinx-doc.org/
23 | exit /b 1
24 | )
25 |
26 | if "%1" == "" goto help
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/template/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "[python]": {
3 | "editor.codeActionsOnSave": {
4 | "source.organizeImports": true
5 | }
6 | },
7 | "editor.rulers": [92],
8 | "eslint.workingDirectories": ["."],
9 | "files.insertFinalNewline": true,
10 | "files.trimFinalNewlines": true,
11 | "files.trimTrailingWhitespace": true,
12 | "prettier.configPath": ".prettierrc.json",
13 | "python.formatting.provider": "black",
14 | "python.linting.flake8Args": [
15 | "--extend-exclude",
16 | "README.rst,README.md,*/settings/*,*/migrations/*",
17 | "--extend-ignore",
18 | "S322,W503,E5110,S101",
19 | "--format",
20 | "grouped",
21 | "--max-line-length",
22 | "92",
23 | "--show-source"
24 | ],
25 | "python.linting.flake8Enabled": true,
26 | "python.testing.pytestEnabled": true,
27 | "python.testing.unittestEnabled": false,
28 | "scss.lint.unknownAtRules": "ignore"
29 | }
30 |
--------------------------------------------------------------------------------
/template/[[project_name]]/user/adapter.py:
--------------------------------------------------------------------------------
1 | from allauth.account.adapter import DefaultAccountAdapter
2 | from django.http import HttpResponseRedirect
3 | from django.urls import reverse
4 | from django_htmx.http import HttpResponseClientRedirect
5 |
6 |
7 | class HTMXAccountAdapter(DefaultAccountAdapter):
8 | def respond_email_verification_sent(self, request, user):
9 | url = reverse("account_email_verification_sent")
10 | if request.htmx:
11 | return HttpResponseClientRedirect(url)
12 | return HttpResponseRedirect(url)
13 |
14 | def post_login(self, request, *args, **kwargs):
15 | response = super().post_login(request, *args, **kwargs)
16 | if request.htmx:
17 | # htmxify the response
18 | response.status_code = 200
19 | response["HX-Redirect"] = response["Location"]
20 | del response["Location"]
21 | return response
22 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "[python]": {
3 | "editor.codeActionsOnSave": {
4 | "source.organizeImports": true
5 | }
6 | },
7 | "editor.rulers": [92],
8 | "eslint.workingDirectories": ["."],
9 | "files.insertFinalNewline": true,
10 | "files.trimFinalNewlines": true,
11 | "files.trimTrailingWhitespace": true,
12 | "prettier.configPath": ".prettierrc.json",
13 | "python.formatting.provider": "black",
14 | "python.linting.flake8Args": [
15 | "--extend-exclude",
16 | "README.rst,README.md,*/settings/*,*/migrations/*",
17 | "--extend-ignore",
18 | "S322,W503,E5110,S101",
19 | "--format",
20 | "grouped",
21 | "--max-line-length",
22 | "92",
23 | "--show-source"
24 | ],
25 | "python.linting.flake8Enabled": true,
26 | "python.testing.pytestEnabled": true,
27 | "python.testing.unittestEnabled": false,
28 | "scss.lint.unknownAtRules": "ignore",
29 | "dotenv.enableAutocloaking": false,
30 | }
31 |
--------------------------------------------------------------------------------
/template/fly.toml.jinja:
--------------------------------------------------------------------------------
1 | # fly.toml app configuration file generated on 2025-01-26T16:37:12-05:00
2 | #
3 | # See https://fly.io/docs/reference/configuration/ for information about how to use this file.
4 | #
5 |
6 | app = "[[project_name]]"
7 | primary_region = 'ewr'
8 | console_command = 'python manage.py shell_plus'
9 |
10 | [build]
11 |
12 | [deploy]
13 | release_command = 'python manage.py migrate --noinput'
14 |
15 | [env]
16 | PORT = "8000"
17 | DJANGO_SETTINGS_MODULE = "[[project_name]].config.settings.prod"
18 | DJANGO_ALLOWED_HOSTS = "*"
19 |
20 | [http_service]
21 | internal_port = 8000
22 | force_https = true
23 | auto_stop_machines = 'suspend'
24 | auto_start_machines = true
25 | min_machines_running = 1
26 | processes = ['app']
27 |
28 | [% raw %]
29 | [[vm]]
30 | memory = '1gb'
31 | cpu_kind = 'shared'
32 | cpus = 1
33 |
34 | [[statics]]
35 | guest_path = '/app/static'
36 | url_prefix = '/static/'
37 | [% endraw %]
38 |
--------------------------------------------------------------------------------
/template/[[project_name]]/templates/account/password_reset_done.html:
--------------------------------------------------------------------------------
1 | {% extends "account/account_base.html" %}
2 | {% load heroicons %}
3 | {% block account_title %}
4 | {% translate "Password Reset" %}
5 | {% endblock account_title %}
6 | {% block account_back %}
7 |
8 | {% heroicon_micro "arrow-long-left" class="inline-block" %} {% translate "Sign In" %}
9 |
10 | {% endblock account_back %}
11 | {% block account_content %}
12 | {% if user.is_authenticated %}
13 | {% include "account/snippets/already_logged_in.html" %}
14 | {% else %}
15 |
16 |
{% translate "We have sent you an e-mail." %}
17 |
18 | {% translate "If you have not received it please check your spam folder. Otherwise contact us if you do not receive it in a few minutes." %}
19 |
20 |
21 | {% endif %}
22 | {% endblock account_content %}
23 |
--------------------------------------------------------------------------------
/template/.stylelintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "stylelint-config-standard-scss",
3 | "reportDescriptionlessDisables": true,
4 | "reportInvalidScopeDisables": true,
5 | "reportNeedlessDisables": true,
6 | "rules": {
7 | "function-url-quotes": ["always", { "except": "empty"}],
8 | "scss/at-import-partial-extension": null,
9 | "scss/at-rule-no-unknown": [ true, {
10 | "ignoreAtRules": [
11 | "extends",
12 | "apply",
13 | "tailwind",
14 | "components",
15 | "utilities",
16 | "screen",
17 | "layer"
18 | ]
19 | }],
20 | "rule-empty-line-before": [
21 | "always",
22 | {
23 | "except": [
24 | "first-nested"
25 | ],
26 | "ignore": [
27 | "after-comment"
28 | ]
29 | }
30 | ]
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/template/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "poetryInstall",
6 | "type": "shell",
7 | "command": "poetry",
8 | "args": ["install"],
9 | "options": {
10 | "cwd": "${workspaceFolder}"
11 | }
12 | },
13 | {
14 | "label": "npmInstall",
15 | "type": "shell",
16 | "command": "npm",
17 | "args": ["install"],
18 | "options": {
19 | "cwd": "${workspaceFolder}"
20 | }
21 | },
22 | {
23 | "label": "playwrightInstall",
24 | "type": "shell",
25 | "command": "poetry",
26 | "args": ["run", "playwright", "install"],
27 | "options": {
28 | "cwd": "${workspaceFolder}"
29 | }
30 | },
31 | {
32 | "label": "setup",
33 | "dependsOn": ["poetryInstall", "npmInstall"]
34 | },
35 | {
36 | "label": "testSetup",
37 | "dependsOn": ["poetryInstall", "npmInstall", "playwrightInstall"]
38 | }
39 | ]
40 | }
41 |
--------------------------------------------------------------------------------
/template/manage.py.jinja:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "[[project_name]].config.settings.local")
7 |
8 | try:
9 | from django.core.management import execute_from_command_line
10 | except ImportError:
11 | # The above import may fail for some other reason. Ensure that the
12 | # issue is really that Django is missing to avoid masking other
13 | # exceptions on Python 2.
14 | try:
15 | import django # pylint: disable=unused-import # noqa
16 | except ImportError:
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? Do you have Direnv installed?"
21 | )
22 | raise
23 |
24 | execute_from_command_line(sys.argv)
25 |
--------------------------------------------------------------------------------
/template/[[project_name]]/templates/account/account_base.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block title %}
3 | Account
4 | {% endblock title %}
5 | {% block content %}
6 |
7 |
8 |
9 | {% block account_back %}
10 | {% endblock account_back %}
11 |
12 |
13 |
18 |
19 | {% block account_title %}
20 | {% endblock account_title %}
21 |
22 | {% block account_content %}
23 | {% endblock account_content %}
24 |
25 |
26 | {% endblock content %}
27 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/header/profile-popover.html:
--------------------------------------------------------------------------------
1 | {% load heroicons %}
2 |
3 | {% component "popover" button_class="group flex items-center rounded-lg w-full p-1 hover:bg-gray-800/10" %}
4 | {% fill "trigger" %}
5 |
Open user menu
6 |
7 |
10 |
11 | {{ user_name }}
12 | {% if show_chevron|default:True %}
13 | {% heroicon_micro "chevron-down" %}
14 | {% endif %}
15 |
16 |
17 | {% endfill %}
18 | {% fill "content" %}
19 |
20 | {% slot "menu_items" default %}
21 | {% endslot %}
22 |
23 | {% endfill %}
24 | {% endcomponent %}
25 |
26 |
--------------------------------------------------------------------------------
/template/[[project_name]]/templates/account/password_reset_from_key.html:
--------------------------------------------------------------------------------
1 | {% extends "account/account_base.html" %}
2 | {% block account_title %}
3 | {% if token_fail %}
4 | {% translate "Bad Token" %}
5 | {% else %}
6 | {% translate "Change Password" %}
7 | {% endif %}
8 | {% endblock account_title %}
9 | {% block account_content %}
10 | {% if token_fail %}
11 |
12 | {% url 'account_reset_password' as passwd_reset_url %}
13 | {% blocktranslate %}The password reset link was invalid, possibly because it has already been used. Please request a new password reset .{% endblocktranslate %}
14 |
15 | {% else %}
16 |
23 | {% endif %}
24 | {% endblock account_content %}
25 |
--------------------------------------------------------------------------------
/docs/source/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # For the full list of built-in configuration values, see the documentation:
4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
5 |
6 | # -- Project information -----------------------------------------------------
7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
8 |
9 | project = "Hydra"
10 | copyright = "2023, Lightmatter Team"
11 | author = "Lightmatter Team"
12 | release = "3.0"
13 |
14 | # -- General configuration ---------------------------------------------------
15 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
16 |
17 | extensions = []
18 |
19 | templates_path = ["_templates"]
20 | exclude_patterns = []
21 |
22 |
23 | # -- Options for HTML output -------------------------------------------------
24 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
25 |
26 | html_theme = "furo"
27 | html_static_path = ["_static"]
28 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/form/widgets/password.html:
--------------------------------------------------------------------------------
1 | {% load replace %}
2 | {% load heroicons %}
3 |
22 |
--------------------------------------------------------------------------------
/template/mise-tasks/db/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # @desc Set up project database
3 | # @depends db:create-user
4 |
5 | set -euo pipefail
6 |
7 | # Source colors
8 | source "$(dirname "$0")/../utils/colors"
9 |
10 | # Get project name from mise config
11 | project_name=$ENV_NAME
12 |
13 | if [ -z "$project_name" ]; then
14 | color_error "Error: project_name not set in mise.toml"
15 | exit 1
16 | fi
17 |
18 | # Check if database exists
19 | if [ "$(psql -tAc "SELECT 1 FROM pg_database WHERE datname='$project_name'" postgres)" != "1" ]; then
20 | color_info "Creating project database..."
21 | if psql postgres -c "create role $project_name with createdb encrypted password '$project_name' login;" &&\
22 | psql postgres -c "alter user $project_name superuser;" &&\
23 | psql postgres -c "create database $project_name with owner $project_name;"; then
24 | color_success "Project database created successfully"
25 | else
26 | color_error "Failed to create project database"
27 | exit 1
28 | fi
29 | else
30 | color_warning "Project database already exists"
31 | fi
32 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/svg/github.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 | Github
7 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/template/[[project_name]]/templates/header/mobile_menu_button.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 | Open main menu
9 |
10 |
12 |
14 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/docs/source/index.rst:
--------------------------------------------------------------------------------
1 | .. Hydra documentation master file, created by
2 | sphinx-quickstart on Thu Sep 22 09:37:05 2022.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Hydra
7 | =======================================================
8 |
9 | About
10 | -----
11 |
12 | Hydra is a robust project template which uses Django 4 on the backend and HTMX, AlpineJS, and Tailwind on the frontend.
13 |
14 | This combination of technologies means:
15 |
16 | - You'll spend less time writing custom Javascript
17 | - Keep frontend code near the locality of behavior
18 | - You'll leverage the strengths of both Django and consise templates to render content quickly and easily
19 | - You'll be easily able to extend this template for customized use cases
20 | - But perhaps the best thing about Hydra is that once you're familiar with it, *it's just fun to use*!
21 |
22 | .. toctree::
23 | :maxdepth: 1
24 | :caption: Contents:
25 |
26 | prerequisites
27 | setup
28 | structure
29 | testing
30 | debugging
31 | cookbook
32 | deployment
33 | contributing
34 |
--------------------------------------------------------------------------------
/template/[[project_name]]/templates/account/snippets/social_login_buttons.html:
--------------------------------------------------------------------------------
1 |
9 |
17 |
25 |
--------------------------------------------------------------------------------
/template/[[project_name]]/config/asgi.py.jinja:
--------------------------------------------------------------------------------
1 | # pylint: disable-all
2 |
3 | """
4 | ASGI config for asgi project.
5 |
6 | It exposes the ASGI callable as a module-level variable named ``application``.
7 |
8 | For more information on this file, see
9 | https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
10 | """
11 |
12 | import os
13 |
14 | from django.core.asgi import get_asgi_application
15 |
16 | # Import websocket application here, so apps from django_application are loaded first
17 | from [[project_name]].config.websocket import websocket_application # noqa isort:skip
18 |
19 | # fmt: off
20 | os.environ.setdefault(
21 | "DJANGO_SETTINGS_MODULE",
22 | "[[project_name]].config.settings.prod",
23 | )
24 | # fmt: on
25 |
26 | django_application = get_asgi_application()
27 | # Apply ASGI middleware here.
28 |
29 |
30 | async def application(scope, receive, send):
31 | if scope["type"] == "http":
32 | await django_application(scope, receive, send)
33 | elif scope["type"] == "websocket":
34 | await websocket_application(scope, receive, send)
35 | else:
36 | raise NotImplementedError(f"Unknown scope type {scope['type']}")
37 |
--------------------------------------------------------------------------------
/template/mise-tasks/utils/colors:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Reset
4 | export COLOR_RESET='\033[0m'
5 |
6 | # Regular Colors
7 | export COLOR_BLACK='\033[0;30m'
8 | export COLOR_RED='\033[0;31m'
9 | export COLOR_GREEN='\033[0;32m'
10 | export COLOR_YELLOW='\033[0;33m'
11 | export COLOR_BLUE='\033[0;34m'
12 | export COLOR_PURPLE='\033[0;35m'
13 | export COLOR_CYAN='\033[0;36m'
14 | export COLOR_WHITE='\033[0;37m'
15 |
16 | # Bold Colors
17 | export COLOR_BOLD_BLACK='\033[1;30m'
18 | export COLOR_BOLD_RED='\033[1;31m'
19 | export COLOR_BOLD_GREEN='\033[1;32m'
20 | export COLOR_BOLD_YELLOW='\033[1;33m'
21 | export COLOR_BOLD_BLUE='\033[1;34m'
22 | export COLOR_BOLD_PURPLE='\033[1;35m'
23 | export COLOR_BOLD_CYAN='\033[1;36m'
24 | export COLOR_BOLD_WHITE='\033[1;37m'
25 |
26 | # Utility functions
27 | color_echo() {
28 | local color="$1"
29 | shift
30 | echo -e "${color}$*${COLOR_RESET}"
31 | }
32 |
33 | color_error() {
34 | color_echo "$COLOR_RED" "$@" >&2
35 | }
36 |
37 | color_warning() {
38 | color_echo "$COLOR_YELLOW" "$@"
39 | }
40 |
41 | color_success() {
42 | color_echo "$COLOR_GREEN" "$@"
43 | }
44 |
45 | color_info() {
46 | color_echo "$COLOR_CYAN" "$@"
47 | }
48 |
--------------------------------------------------------------------------------
/template/[[project_name]]/templates/header/end.html:
--------------------------------------------------------------------------------
1 | {% if not request or not request.user.is_authenticated %}
2 |
8 | {% else %}
9 |
10 | {% component "profile-popover"
11 | avatar_url="https://images.unsplash.com/photo-1601814933824-fd0b574dd592?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1112&q=80"
12 | user_name=request.user.get_full_name
13 | user_email=request.user.email
14 | %}
15 | {% include "header/profile_menu_item.html" with label="Your Profile" icon="user" only %}
16 | {% include "header/profile_menu_item.html" with label="Settings" icon="cog-6-tooth" only %}
17 | {% url 'account_logout' as logout_url %}
18 | {% include "header/profile_menu_item.html" with label="Sign out" href=logout_url icon="arrow-right-start-on-rectangle" only %}
19 | {% endcomponent %}
20 |
21 | {% endif %}
22 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/form/widgets/radio.html:
--------------------------------------------------------------------------------
1 | {# todo: get widget.required working, but need peer-invalid error messages first #}
2 |
6 | {% for group, options, index in widget.optgroups %}
7 | {% if group %}
{{ group }} {% endif %}
8 | {% for option in options %}
9 |
10 |
16 |
19 |
20 | {{ option.label }}
21 |
22 |
23 | {% endfor %}
24 | {% endfor %}
25 |
26 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/svg/dribbble.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 | Dribble
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/template/Dockerfile.jinja:
--------------------------------------------------------------------------------
1 | FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS builder
2 | ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
3 | WORKDIR /app
4 | RUN --mount=type=cache,target=/root/.cache/uv \
5 | --mount=type=bind,source=uv.lock,target=uv.lock \
6 | --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
7 | uv sync --frozen --no-install-project --no-dev
8 | COPY . /app
9 | RUN --mount=type=cache,target=/root/.cache/uv \
10 | uv sync --frozen --no-dev
11 |
12 |
13 | FROM node:20-slim as js-builder
14 | WORKDIR /app
15 | COPY package*.json ./
16 | RUN npm ci
17 | COPY . .
18 | RUN npm run build
19 |
20 | # In your main stage:
21 |
22 | FROM python:3.13-slim-bookworm as web
23 | # Copy the application from the builder
24 | COPY --from=builder --chown=app:app /app /app
25 | COPY --from=js-builder /app/[[project_name]]/static_source/assets /app/[[project_name]]/static_source/assets
26 | COPY --from=js-builder /app/[[project_name]]/static_source/manifest.json /app/[[project_name]]/static_source/
27 |
28 | # Place executables in the environment at the front of the path
29 | WORKDIR /app
30 | ENV PATH="/app/.venv/bin:$PATH"
31 | RUN python manage.py collectstatic --noinput --settings [[project_name]].config.settings.build
32 | EXPOSE 8000
33 |
34 | CMD ["gunicorn"]
35 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/alert/alert.html:
--------------------------------------------------------------------------------
1 | {% load heroicons %}
2 |
3 |
17 |
18 |
{% heroicon_solid icon class="h-5 w-5" %}
19 |
22 |
23 |
24 |
27 | Dismiss
28 | {% heroicon_mini "x-mark" class="h-5 w-5" %}
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/template/[[project_name]]/templates/footer.html.jinja:
--------------------------------------------------------------------------------
1 |
21 |
--------------------------------------------------------------------------------
/template/package.json.jinja:
--------------------------------------------------------------------------------
1 | {
2 | "name": "[[project_name]]",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "dev": "vite",
6 | "build": "tsc --noEmit && vite build",
7 | "serve": "vite preview",
8 | "test": "vitest",
9 | "coverage": "vitest run --coverage"
10 | },
11 | "devDependencies": {
12 | "@eslint/js": "^9.17.0",
13 | "@tailwindcss/forms": "^0.5.9",
14 | "@types/alpinejs": "^3.13.11",
15 | "@types/alpinejs__focus": "^3.13.4",
16 | "@types/alpinejs__mask": "^3.13.4",
17 | "@types/js-cookie": "^3.0.6",
18 | "autoprefixer": "^10.4.20",
19 | "eslint": "^9.17.0",
20 | "fast-glob": "^3.3.2",
21 | "happy-dom": "^15.11.7",
22 | "postcss": "^8.4.49",
23 | "postcss-nested": "^7.0.2",
24 | "sass": "^1.83.0",
25 | "stylelint": "^16.12.0",
26 | "stylelint-config-standard-scss": "^14.0.0",
27 | "tailwindcss": "^3.4.17",
28 | "typescript": "^5.7.2",
29 | "typescript-eslint": "^8.18.1",
30 | "vite": "^6.0.5",
31 | "vitest": "^2.1.8"
32 | },
33 | "dependencies": {
34 | "@alpinejs/focus": "^3.14.7",
35 | "@alpinejs/mask": "^3.14.7",
36 | "@alpinejs/ui": "^3.14.7",
37 | "alpinejs": "^3.14.7",
38 | "flatpickr": "^4.6.13",
39 | "htmx.org": "^2.0.4",
40 | "js-cookie": "^3.0.5",
41 | "tom-select": "^2.4.1"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/template/[[project_name]]/static_source/css/base/_colors.scss:
--------------------------------------------------------------------------------
1 | // Sass Color variables
2 | $primary: #2d36a8;
3 | $primary-focus: #00075e;
4 | $accent: #111827;
5 | $accent-focus: #030509;
6 | $error: #f87171;
7 | $error-focus: #991b1b;
8 | $success: #4ade80;
9 | $success-focus: #166534;
10 | $white: #fff;
11 | $eggshell: #fef2f2;
12 | $grey: #d1d5db;
13 | $dark-grey: #d8d8d8;
14 | $grey-focus: #e5e8ed;
15 | $black: #000;
16 | $info: #60a5fa;
17 | $info-focus: #1e40af;
18 | $info-content: #eff6ff;
19 |
20 | // CSS Custom Properties for use in tailwind config
21 | :root {
22 | --accent: #{$accent};
23 | --accent-focus: #{$accent-focus};
24 | --accent-content: #{$white};
25 | --primary: #{$primary};
26 | --primary-focus: #{$primary-focus};
27 | --primary-content: #{$white};
28 | --secondary: #{$grey};
29 | --secondary-focus: #{$grey-focus};
30 | --secondary-content: #{$black};
31 | --error: #{$error};
32 | --error-focus: #{$error-focus};
33 | --error-content: #{$eggshell};
34 | --warning: #facc15;
35 | --warning-focus: #854d0e;
36 | --warning-content: #fefce8;
37 | --success: #{$success};
38 | --success-focus: #{$success-focus};
39 | --success-content: #{$eggshell};
40 | --info: #{$info};
41 | --info-focus: #{$info-focus};
42 | --info-content: #{$info-content};
43 | --slate: #54565a;
44 | --silver: #b1b1b1;
45 | }
46 |
--------------------------------------------------------------------------------
/template/[[project_name]]/static_source/img/favicons/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
14 |
15 |
16 |
21 |
22 |
27 |
28 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/template/[[project_name]]/templates/account/password_reset.html:
--------------------------------------------------------------------------------
1 | {% extends "account/account_base.html" %}
2 | {% load heroicons %}
3 | {% block account_title %}
4 | {% translate "Reset Password" %}
5 | {% endblock account_title %}
6 | {% block account_back %}
7 |
8 | {% heroicon_micro "arrow-long-left" class="inline-block" %} {% translate "Sign In" %}
9 |
10 | {% endblock account_back %}
11 | {% block account_content %}
12 | {% if user.is_authenticated %}
13 | {% include "account/snippets/already_logged_in.html" %}
14 | {% else %}
15 |
16 |
{% translate "Forgotten your password?" %}
17 |
{% translate "Enter your e-mail address below, and we'll send you an e-mail allowing you to reset it." %}
18 |
19 |
28 | {% translate "Please contact us if you have any trouble resetting your password." %}
29 | {% endif %}
30 | {% endblock account_content %}
31 |
--------------------------------------------------------------------------------
/template/[[project_name]]/static_source/css/base/_index.scss:
--------------------------------------------------------------------------------
1 | @forward "colors";
2 | @forward "typography";
3 | @forward "fonts";
4 | @forward "forms";
5 | @forward "flatpickr";
6 |
7 | /* sticky footer */
8 | body {
9 | display: flex;
10 | flex-direction: column;
11 | min-height: 100vh;
12 | height: 100%;
13 | overflow-x: hidden;
14 |
15 | // Example font-family inclusion
16 | // font-family: "GT Walsheim Pro", sans-serif;
17 | }
18 |
19 | html {
20 | -webkit-tap-highlight-color: transparent;
21 |
22 | // For better cross-browser consistency
23 | width: 100vw;
24 | }
25 |
26 |
27 | // for center positioned elements, that should stay in the same position
28 | // when the y-scrollbar is present or not
29 | // you could also do this as "padding-left: calc(100vw - 100%);"
30 | // which will shrink the viewport vs hiding under the scrollbar
31 | @mixin no-jitter-scrollbar {
32 | margin-right: calc(-1 * (100vw - 100%));
33 | }
34 |
35 | #app {
36 | flex: 1 0 auto;
37 | flex-direction: column;
38 |
39 | @include no-jitter-scrollbar;
40 | }
41 |
42 | header {
43 | @include no-jitter-scrollbar;
44 | }
45 |
46 | footer {
47 | flex-shrink: 0;
48 |
49 | @include no-jitter-scrollbar;
50 | }
51 |
52 | #loading-body {
53 | position: fixed;
54 | top: 50%;
55 | z-index: 40;
56 | }
57 |
58 | /* alpine */
59 | [x-cloak] {
60 | display: none !important;
61 | }
62 |
--------------------------------------------------------------------------------
/template/[[project_name]]/static_source/js/links.ts:
--------------------------------------------------------------------------------
1 | export function isExternalLink(link: HTMLAnchorElement) {
2 | const { href } = link;
3 |
4 | return !(
5 | !href
6 | || href.startsWith(window.location.origin)
7 | || href[0] === "?"
8 | || href[0] === "/"
9 | || href[0] === "#"
10 | || href.substring(0, 4) === "tel:"
11 | || href.substring(0, 7) === "mailto:"
12 | );
13 | }
14 |
15 | export function isCurrentPage(link: HTMLAnchorElement) {
16 | //javascript controls tend to point to #
17 | if (link.getAttribute('href') === '#') {
18 | return false;
19 | }
20 |
21 | const currentUrl = window.location.href;
22 | const currentPath = window.location.pathname;
23 | const href = link.href.split("#")[0];
24 |
25 | return href === currentUrl || href === currentPath;
26 | }
27 |
28 | export default function linksInit() {
29 | const links = document.getElementsByTagName("a");
30 |
31 | Array.from(links).forEach((link) => {
32 | link.classList.remove("active");
33 |
34 | if (isExternalLink(link)) {
35 | link.target = "_blank";
36 | }
37 |
38 | if (isCurrentPage(link)) {
39 | link.classList.add("active");
40 | }
41 | });
42 | }
43 |
44 | if (typeof document !== "undefined") {
45 | document.addEventListener("htmx:pushedIntoHistory", linksInit);
46 | document.addEventListener("DOMContentLoaded", linksInit);
47 | }
48 |
--------------------------------------------------------------------------------
/template/[[project_name]]/config/wsgi.py.jinja:
--------------------------------------------------------------------------------
1 | # pylint: disable-all
2 |
3 | """
4 | WSGI config for [[project_name]] project.
5 | This module contains the WSGI application used by Django's development server
6 | and any production WSGI deployments. It should expose a module-level variable
7 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
8 | this application via the ``WSGI_APPLICATION`` setting.
9 | Usually you will have the standard Django WSGI application here, but it also
10 | might make sense to replace the whole Django WSGI application with a custom one
11 | that later delegates to the Django one. For example, you could introduce WSGI
12 | middleware here, or combine a Django application with an application of another
13 | framework.
14 | """
15 |
16 | import os
17 |
18 | from django.core.wsgi import get_wsgi_application
19 |
20 | # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks
21 | # if running multiple sites in the same mod_wsgi process. To fix this, use
22 | # mod_wsgi daemon mode with each site in its own daemon process, or use
23 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "[[project_name]].config.settings.production")
24 |
25 | # This application object is used by any WSGI server configured to use this
26 | # file. This includes Django's development server, if the WSGI_APPLICATION
27 | # setting points here.
28 | application = get_wsgi_application()
29 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/svg/apple.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 | Apple
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/template/[[project_name]]/static_source/img/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
17 |
18 |
23 |
24 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
42 | DJANGO-HYDRA
43 |
44 |
45 |
--------------------------------------------------------------------------------
/template/[[project_name]]/util/middleware.py:
--------------------------------------------------------------------------------
1 | import zoneinfo
2 |
3 | from django.contrib.messages import get_messages
4 | from django.template.loader import render_to_string
5 | from django.utils import timezone
6 |
7 |
8 | def attach_messages(response):
9 | if not (req_messages := get_messages(response._request)).used:
10 | messages = render_to_string(
11 | "messages.html",
12 | {"messages": req_messages}, # NOQA
13 | )
14 | response.content = response.content + messages.encode(response.charset)
15 | return response
16 |
17 |
18 | class HTMXMessageMiddleware:
19 | def __init__(self, get_response):
20 | self.get_response = get_response
21 |
22 | def __call__(self, request):
23 | response = self.get_response(request)
24 | return response
25 |
26 | def process_template_response(self, request, response):
27 | if request.htmx and not request.htmx.boosted:
28 | response.add_post_render_callback(attach_messages)
29 | return response
30 |
31 |
32 | class TimezoneMiddleware:
33 | def __init__(self, get_response):
34 | self.get_response = get_response
35 |
36 | def __call__(self, request):
37 | tzname = request.headers.get("X-Timezone", None)
38 | if tzname:
39 | timezone.activate(zoneinfo.ZoneInfo(tzname))
40 | else:
41 | timezone.deactivate()
42 | return self.get_response(request)
43 |
--------------------------------------------------------------------------------
/template/[[project_name]]/static_source/img/logomark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
14 |
15 |
16 |
21 |
22 |
27 |
28 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/template/[[project_name]]/templates/account/signup.html:
--------------------------------------------------------------------------------
1 | {% extends "account/account_base.html" %}
2 | {% block title %}
3 | {{ block.super }}| {% translate "Register" %}
4 | {% endblock title %}
5 | {% block account_title %}
6 | {% translate "Register" %}
7 | {% endblock account_title %}
8 | {% block account_content %}
9 |
24 | {% translate "Already have an account?" %}
25 |
26 |
29 |
30 | {% translate "Or" %}
31 |
32 |
33 |
34 |
{% include "account/snippets/social_login_buttons.html" %}
35 |
36 | {% endblock account_content %}
37 |
--------------------------------------------------------------------------------
/template/[[project_name]]/util/tests.py:
--------------------------------------------------------------------------------
1 | import time
2 | from datetime import datetime
3 |
4 | import pytest
5 | from django.conf import settings
6 | from django.core import mail
7 | from django.core.files.uploadedfile import SimpleUploadedFile
8 |
9 | from .models import TestFileModel
10 | from .util import file_url
11 |
12 |
13 | def test_file_url():
14 | file_url_obj = file_url("foo")
15 | assert file_url_obj.category == "foo"
16 |
17 | timestamp = int(time.time())
18 | actual = file_url_obj("trash", "some_filename")
19 | now = datetime.now()
20 |
21 | expected = f"uploads/foo/{now:%Y/%m/%d}/{timestamp}/some_filename"
22 | assert actual == expected
23 |
24 |
25 | @pytest.mark.django_db
26 | def test_file_upload():
27 | fake_file = SimpleUploadedFile("some_file.txt", b"asdf", content_type="text")
28 | now = datetime.now()
29 | timestamp = int(time.time())
30 |
31 | file_field_url = TestFileModel.objects.create(file_field=fake_file).file_field.url
32 | expected = f"{settings.MEDIA_URL}uploads/filez/{now:%Y/%m/%d}/{timestamp}/some_file.txt"
33 | assert file_field_url == expected
34 |
35 |
36 | def test_send_email(mailoutbox):
37 | mail.send_mail("subject", "body", "from@lightmatter.com", ["to@lightmatter.com"])
38 | assert len(mailoutbox) == 1
39 |
40 | m = mailoutbox[0]
41 | assert m.subject == "subject"
42 | assert m.body == "body"
43 | assert m.from_email == "from@lightmatter.com"
44 | assert list(m.to) == ["to@lightmatter.com"]
45 |
--------------------------------------------------------------------------------
/template/[[project_name]]/templates/account/login.html:
--------------------------------------------------------------------------------
1 | {% extends "account/account_base.html" %}
2 | {% block title %}
3 | {{ block.super }} | Welcome back!
4 | {% endblock title %}
5 | {% block account_title %}
6 | {% translate "Sign In" %}
7 | {% endblock account_title %}
8 | {% block account_content %}
9 |
24 |
25 | {% translate "Need an account?" %} {% translate "Register" %}
26 |
27 |
28 |
31 |
32 | {% translate "Or" %}
33 |
34 |
35 |
36 |
{% include "account/snippets/social_login_buttons.html" %}
37 |
38 | {% endblock account_content %}
39 |
--------------------------------------------------------------------------------
/template/[[project_name]]/user/forms.py:
--------------------------------------------------------------------------------
1 | from allauth.account.forms import LoginForm as AllAuthLoginForm
2 | from allauth.account.forms import SignupForm as AllAuthSignupForm
3 | from django import forms
4 | from django.utils.translation import gettext_lazy as _
5 |
6 | from .models import User
7 |
8 |
9 | class LoginForm(AllAuthLoginForm):
10 | remember = forms.BooleanField(
11 | help_text=_("For 2 weeks"),
12 | label=_("Remember Me"),
13 | required=False,
14 | )
15 |
16 | def __init__(self, *args, **kwargs):
17 | super().__init__(*args, **kwargs)
18 |
19 |
20 | class SignupForm(AllAuthSignupForm):
21 | first_name = forms.CharField(
22 | label=_("First Name"),
23 | min_length=1,
24 | max_length=User._meta.get_field("first_name").max_length,
25 | widget=forms.TextInput(
26 | attrs={"placeholder": _("First Name"), "autocomplete": "given-name"},
27 | ),
28 | )
29 |
30 | last_name = forms.CharField(
31 | label=_("Last Name"),
32 | min_length=1,
33 | max_length=User._meta.get_field("last_name").max_length,
34 | widget=forms.TextInput(
35 | attrs={"placeholder": _("Last Name"), "autocomplete": "family-name"},
36 | ),
37 | )
38 |
39 | def __init__(self, *args, **kwargs):
40 | super().__init__(*args, **kwargs)
41 | self.fields["email"].label = "Email"
42 | self.fields["email2"].label = "Confirm Email"
43 | self.fields["password2"].label = "Confirm Password"
44 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/svg/django.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 | Django
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/alert/alert.py:
--------------------------------------------------------------------------------
1 | from django_components import Component, register
2 |
3 |
4 | @register("alert")
5 | class Alert(Component):
6 | """An alert component that works with Django's messages framework.
7 |
8 | Usage:
9 | {% component "alert" message=message %}{% endcomponent %}
10 | """
11 |
12 | template_name = "alert.html"
13 |
14 | level_icons = {
15 | "error": "exclamation-circle",
16 | "warning": "exclamation-triangle",
17 | "success": "check-circle",
18 | "info": "information-circle",
19 | "debug": "information-circle",
20 | }
21 |
22 | level_classes = {
23 | "debug": "bg-info/5 text-info border border-info/10 ring-1 ring-info/10",
24 | "info": "bg-info/5 text-info border border-info/10 ring-1 ring-info/10",
25 | "success": "bg-success/5 text-success border border-success/10 ring-1 ring-success/10",
26 | "warning": "bg-warning/5 text-warning border border-warning/10 ring-1 ring-warning/10",
27 | "error": "bg-error/5 text-error border border-error/10 ring-1 ring-error/10",
28 | }
29 |
30 | def get_context_data(self, message):
31 | level_tag = message.level_tag
32 | level_class = self.level_classes.get(level_tag, self.level_classes["info"])
33 | icon = self.level_icons.get(level_tag, self.level_icons["info"])
34 |
35 | return {
36 | "message": message,
37 | "icon": icon,
38 | "level_class": level_class,
39 | "timeout": 9000,
40 | }
41 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/form/toggle.html:
--------------------------------------------------------------------------------
1 |
2 |
4 | {% if field.label %}
5 | {% component "label" field=field %}
6 | {{ field.label }}
7 | {% endcomponent %}
8 | {% endif %}
9 |
16 |
23 |
28 |
30 |
31 |
32 |
33 | {% if field.errors %}
{{ field.errors }}
{% endif %}
34 | {% if field.help_text %}
35 |
{{ field.help_text|safe }}
37 | {% endif %}
38 |
39 |
--------------------------------------------------------------------------------
/docs/source/contributing.rst:
--------------------------------------------------------------------------------
1 | Contributing to the Template
2 | ============================
3 |
4 |
5 | 1. Ensure git is configured globally to use ``main`` as the default branch name.
6 |
7 | .. code-block:: console
8 |
9 | $ git config --global init.defaultBranch main
10 |
11 |
12 | 2. Follow the steps in :ref:`setup` to create a new project.
13 |
14 | 3. Make your changes in this new project, then commit to git on a new feature branch.
15 |
16 | 4. From this project's directory (default ``django-hydra``) run retrocookie.
17 |
18 | This will attempt to take the git diff of the prior commit and apply it back to the template.
19 |
20 | .. code-block:: console
21 |
22 | $ poetry shell # enter the poetry virtual env first
23 | $ retrocookie --branch=your-branch-name ../your-project-name
24 |
25 |
26 | .. warning::
27 |
28 | When adding new dependencies to a project, always delete the `poetry.lock` file and recreate it before committing, otherwise it won't merge correctly.
29 |
30 | Additionally, retrocookie does not currently support ignoring jinja syntax. Therefore you will need to manually backport any changes to jinja templates.
31 |
32 | The documentation for retrocookie is here: https://pypi.org/project/retrocookie/
33 |
34 |
35 | Upcoming Features
36 | =================
37 |
38 | Things we still want to do
39 |
40 | * caching everything possible (middleware for sure)
41 | * user useradmin
42 | * django-secure
43 | * django robots
44 | * user feedback
45 | * add django password validators
46 | * Front end updates
47 | * SEO compatibility scrub
48 | * Accessibility compatibility scrub
49 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/svg/google.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 | Google
8 |
9 |
12 |
15 |
18 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/svg/linkedin.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 | Linkedin
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/template/[[project_name]]/templates/base.html:
--------------------------------------------------------------------------------
1 | {% load django_vite %}
2 | {% load django_htmx %}
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | {% block opengraph %}
12 | {% include "components/open_graph_tags.html" with title="TODO | Home" url="TODO" description="TODO" image_url="TODO" %}
13 | {% endblock opengraph %}
14 |
15 | {% vite_hmr_client %}
16 | {% vite_asset "css/styles.js" %}
17 | {% vite_asset "js/components.ts" %}
18 | {% vite_asset "js/main.ts" %}
19 | {% django_htmx_script %}
20 |
21 | {% block title %}
22 | {% endblock title %}
23 |
24 | {% block extra_head %}
25 | {% endblock extra_head %}
26 |
27 |
31 | {% include "header/base.html" %}
32 |
33 | {% block content %}
34 | {% endblock content %}
35 | {# hx-preserve persists this element on htmx swaps
36 | so even if swapping an entire new page this element exists
37 | for content to be swapped in as an oob-swap #}
38 |
39 | {% include "messages.html" %}
40 |
41 |
42 | {% include "footer.html" %}
43 |
44 |
45 |
--------------------------------------------------------------------------------
/template/[[project_name]]/user/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
3 | from django.contrib.auth.forms import UserChangeForm as DjangoUserChangeForm
4 | from django.contrib.auth.forms import UserCreationForm as DjangoUserCreationForm
5 | from django.utils.translation import gettext_lazy as _
6 |
7 | from .models import User
8 |
9 |
10 | class UserCreationForm(DjangoUserCreationForm):
11 | class Meta:
12 | model = User
13 | fields = ("email",)
14 |
15 |
16 | class UserChangeForm(DjangoUserChangeForm):
17 | class Meta:
18 | model = User
19 | fields = "__all__"
20 |
21 |
22 | @admin.register(User)
23 | class UserAdmin(DjangoUserAdmin):
24 | fieldsets = (
25 | (None, {"fields": ("email", "password")}),
26 | (_("Personal info"), {"fields": ("first_name", "last_name")}),
27 | (
28 | _("Permissions"),
29 | {
30 | "fields": (
31 | "is_active",
32 | "is_staff",
33 | "is_superuser",
34 | "groups",
35 | "user_permissions",
36 | ),
37 | },
38 | ),
39 | (_("Important dates"), {"fields": ("last_login", "created")}),
40 | )
41 | add_fieldsets = ((None, {"classes": ("wide",), "fields": ("email", "password1", "password2")}),)
42 |
43 | add_form = UserCreationForm
44 | form = UserChangeForm
45 | list_per_page = 25
46 | search_fields = ("email", "first_name", "last_name")
47 | readonly_fields = ("created", "last_login")
48 | list_display = ("email", "first_name", "last_name", "created", "is_superuser")
49 | ordering = ("email",)
50 |
--------------------------------------------------------------------------------
/template/[[project_name]]/static_source/js/forms/date_datetime.ts:
--------------------------------------------------------------------------------
1 | import AlpineInstance, { AlpineComponent } from "alpinejs";
2 | import flatpickr from "flatpickr";
3 |
4 | import "flatpickr/dist/themes/light.css";
5 | import inputListener from "./common";
6 |
7 | interface DateTime{
8 | //callback requires indexing to string and symbol
9 | [key: string]: unknown;
10 | [key: symbol]: unknown;
11 | //real types
12 | eventName: string;
13 | value: string;
14 | enableTime: boolean;
15 | picker: flatpickr.Instance | null;
16 | }
17 |
18 |
19 | const dateTime = (...args: unknown[]): AlpineComponent => {
20 | const [eventName, value, enableTime] = args as [string, string, boolean];
21 | return {
22 | eventName,
23 | value,
24 | enableTime,
25 | picker: null,
26 | active: false,
27 | init() {
28 | inputListener.call(this);
29 |
30 | if (this.value === "None") {
31 | this.value = "";
32 | }
33 |
34 | // see https://flatpickr.js.org/formatting/
35 | const dateFormat = enableTime ? "m/d/Y H:i" : "m/d/Y";
36 |
37 | this.picker = flatpickr(this.$refs.picker, {
38 | mode: "single",
39 | enableTime,
40 | dateFormat,
41 | allowInput: true,
42 | defaultDate: value,
43 | onChange: (_, dateString) => {
44 | this.value = dateString;
45 | },
46 | });
47 |
48 | this.$watch("value", () => {
49 | //this.picker?.setDate(this.value); //this is in the alpine docs but doesn't work with allow input
50 | if (this.eventName !== "") this.$dispatch(this.eventName, { value: this.value });
51 | });
52 |
53 | if (this.value) {
54 | this.active = true;
55 | }
56 | },
57 | }
58 | };
59 |
60 | AlpineInstance.data("dateTime", dateTime);
61 |
--------------------------------------------------------------------------------
/template/[[project_name]]/home/views.py:
--------------------------------------------------------------------------------
1 | # Create your views here.
2 |
3 | from django.contrib import messages
4 | from django.template.response import TemplateResponse
5 | from django.views.defaults import (
6 | bad_request,
7 | page_not_found,
8 | permission_denied,
9 | server_error,
10 | )
11 | from django.views.generic.edit import FormView
12 | from django_htmx.http import HttpResponseClientRedirect, HttpResponseClientRefresh
13 |
14 | from .forms import TestForm
15 |
16 |
17 | def error(request):
18 | """Generate an exception. Useful for e.g. configuring Sentry"""
19 | raise Exception("Make response code 500!")
20 |
21 |
22 | def current_time(request):
23 | """Generate the current time. Useful for testing htmx"""
24 | messages.info(request, "updated the current time")
25 | return TemplateResponse(request, "samples/current_time.html")
26 |
27 |
28 | def test_message_redirect(request):
29 | messages.info(request, "testing redirect")
30 | return HttpResponseClientRedirect("/")
31 |
32 |
33 | def test_message_refresh(request):
34 | messages.info(request, "testing refresh")
35 | return HttpResponseClientRefresh()
36 |
37 |
38 | def FourHundy(request, exception):
39 | return bad_request(request, exception, template_name="400.html")
40 |
41 |
42 | def FourOhThree(request, exception):
43 | return permission_denied(request, exception, template_name="403.html")
44 |
45 |
46 | def FourOhFour(request, exception):
47 | return page_not_found(request, exception, template_name="404.html")
48 |
49 |
50 | def WorkedLocally(request):
51 | return server_error(request, template_name="500.html")
52 |
53 |
54 | class FormTestView(FormView):
55 | template_name = "home/form_test.html"
56 | form_class = TestForm
57 | success_url = "/"
58 |
59 | def form_valid(self, form):
60 | return super().form_invalid(form)
61 |
--------------------------------------------------------------------------------
/docs/source/debugging.rst:
--------------------------------------------------------------------------------
1 | Local Development & Debugging
2 | ==============================
3 |
4 | Running the local development servers
5 | --------------------------------------
6 |
7 | This app uses vite to compile/transpile assets. The app is equipped to be served from `127.0.0.1:8000` or `localhost:8000`.
8 |
9 | First run the python server:
10 |
11 | .. code-block:: console
12 |
13 | $ ./manage.py runserver_plus
14 |
15 | Then in a new tab, run the vite server:
16 |
17 | .. code-block:: console
18 |
19 | $ npm run dev
20 |
21 | Debugging
22 | ----------
23 |
24 | To access a python shell pre-populated with Django models and local env:
25 |
26 | .. code-block:: console
27 |
28 | $ ./manage.py shell_plus
29 |
30 | To add a breakpoint in your python code, add the following code to your `.bashrc` or `.zshrc`:
31 |
32 | .. code-block:: console
33 |
34 | $ export PYTHONBREAKPOINT="pudb.set_trace"
35 |
36 | Then add the following to your python code:
37 |
38 | .. code-block:: python
39 |
40 | breakpoint()
41 |
42 | If the above fails or you prefer a more immediate solution, you can add the following to your code:
43 |
44 | .. code-block:: python
45 |
46 | import pudb; pu.db
47 |
48 | As an alternative to pudb and its debugger, this project also has the IPython debugger (ipdb). You can access ipdb by adding the following to your code:
49 |
50 | .. code-block:: python
51 |
52 | import ipdb;
53 | ipdb.set_trace()
54 |
55 | For ease of local development, `icecream `_ is preconfigured and ready to use.
56 |
57 | Logging
58 | -------
59 |
60 | Logging is configured in the base.py settings. To use the logger in your backend code you can add the following:
61 |
62 | .. code-block:: python
63 |
64 | import logging
65 |
66 | logger = logging.getLogger(__name__)
67 |
68 | logger.info("this is an info level log")
69 |
--------------------------------------------------------------------------------
/template/[[project_name]]/static_source/css/components/_buttons.scss:
--------------------------------------------------------------------------------
1 | @layer components {
2 | .btn {
3 | @apply inline-flex items-center justify-center;
4 | @apply rounded focus:outline-none focus:ring-2 focus:ring-offset-2;
5 |
6 | /* Base spacing */
7 | @apply px-4 py-2;
8 | @apply space-x-2;
9 |
10 | /* Size variants */
11 | &-sm {
12 | @apply px-3 py-1.5 text-sm space-x-1.5;
13 | }
14 |
15 | &-md {
16 | @apply px-4 py-2 text-base space-x-2;
17 | }
18 |
19 | &-lg {
20 | @apply px-5 py-2.5 text-lg space-x-2.5;
21 | }
22 |
23 | /* Color variants using our theme variables */
24 | &.btn-primary {
25 | @apply bg-primary text-primary-content;
26 | @apply hover:bg-primary-focus;
27 | @apply focus:ring-primary;
28 |
29 | &.btn-outline {
30 | @apply bg-transparent;
31 | @apply border border-primary;
32 | @apply text-primary;
33 | @apply hover:bg-primary hover:text-primary-content;
34 | @apply focus:ring-primary;
35 | }
36 | }
37 |
38 | &.btn-secondary {
39 | @apply bg-secondary text-secondary-content;
40 | @apply hover:bg-secondary-focus;
41 | @apply focus:ring-secondary;
42 |
43 | &.btn-outline {
44 | @apply bg-transparent;
45 | @apply border border-secondary;
46 | @apply text-secondary;
47 | @apply hover:bg-secondary-focus hover:text-secondary-content;
48 | @apply focus:ring-secondary;
49 | }
50 | }
51 |
52 |
53 |
54 | /* Disabled state */
55 | &[disabled], &.disabled {
56 | @apply opacity-50 cursor-not-allowed pointer-events-none;
57 | }
58 |
59 | /* HTMX States */
60 | &.htmx-request {
61 | @apply opacity-75 cursor-wait;
62 | }
63 |
64 | &.htmx-swapping {
65 | @apply opacity-50 transition-opacity duration-200;
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/template/[[project_name]]/static_source/css/base/_flatpickr.scss:
--------------------------------------------------------------------------------
1 | // stylelint-disable selector-class-pattern -- We can't control the class names so:
2 |
3 | .flatpickr-calendar {
4 | width: auto !important;
5 | padding: 10px !important;
6 | border-radius: 15px !important;
7 |
8 | @apply bg-white #{!important};
9 | }
10 |
11 | .flatpickr-current-month .numInputWrapper {
12 | margin-left: 0.5em !important;
13 | }
14 |
15 | .flatpickr-months .flatpickr-month,
16 | .flatpickr-monthDropdown-months,
17 | .flatpickr-weekdays,
18 | span.flatpickr-weekday {
19 | @apply bg-white #{!important};
20 | }
21 |
22 | .flatpickr-current-month input.cur-year,
23 | .flatpickr-current-month .flatpickr-monthDropdown-months {
24 | border-radius: 3px !important;
25 | }
26 |
27 | .flatpickr-months {
28 | position: relative;
29 | }
30 |
31 | .flatpickr-months .flatpickr-prev-month:hover svg,
32 | .flatpickr-months .flatpickr-next-month:hover svg {
33 | @apply fill-red-500 #{!important};
34 | }
35 |
36 | .flatpickr-weekdays {
37 | padding: 25px 0 15px;
38 | }
39 |
40 | .flatpickr-innerContainer {
41 | border: 0 !important;
42 | }
43 |
44 | .flatpickr-days {
45 | border: 0 !important;
46 | }
47 |
48 | .flatpickr-day.selected,
49 | .flatpickr-day.startRange,
50 | .flatpickr-day.endRange,
51 | .flatpickr-day.selected.inRange,
52 | .flatpickr-day.startRange.inRange,
53 | .flatpickr-day.endRange.inRange,
54 | .flatpickr-day.selected:focus,
55 | .flatpickr-day.startRange:focus,
56 | .flatpickr-day.endRange:focus,
57 | .flatpickr-day.selected:hover,
58 | .flatpickr-day.startRange:hover,
59 | .flatpickr-day.endRange:hover,
60 | .flatpickr-day.selected.prevMonthDay,
61 | .flatpickr-day.startRange.prevMonthDay,
62 | .flatpickr-day.endRange.prevMonthDay,
63 | .flatpickr-day.selected.nextMonthDay,
64 | .flatpickr-day.startRange.nextMonthDay,
65 | .flatpickr-day.endRange.nextMonthDay {
66 | @apply bg-red-500 border-red-500 #{!important};
67 | }
68 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/svg/instagram.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 | Instagram
7 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/svg/discord.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 | Discord
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/template/[[project_name]]/config/urls.py.jinja:
--------------------------------------------------------------------------------
1 | # flake8: noqa: F811
2 | from django.conf import settings
3 | from django.conf.urls import handler400, handler403, handler404, handler500
4 | from django.contrib import admin
5 | from django.urls import include, path, re_path
6 |
7 | from [[project_name]].home.views import FourHundy, FourOhFour, FourOhThree, WorkedLocally
8 |
9 | handler400 = FourHundy
10 | handler403 = FourOhThree
11 | handler404 = FourOhFour
12 | handler500 = WorkedLocally
13 |
14 |
15 | urlpatterns = []
16 |
17 | if "silk" in settings.INSTALLED_APPS:
18 | urlpatterns += [path("silk", include("silk.urls", namespace="silk"))]
19 |
20 | if settings.DEBUG:
21 | # This allows the error pages to be debugged during development, just visit
22 | # these url in browser to see how these error pages look like.
23 | urlpatterns += [
24 | path(
25 | "400/",
26 | handler400,
27 | kwargs={"exception": Exception("Bad Request!")},
28 | ),
29 | path(
30 | "403/",
31 | handler403,
32 | kwargs={"exception": Exception("Permission Denied")},
33 | ),
34 | path(
35 | "404/",
36 | handler404,
37 | kwargs={"exception": Exception("Page not Found")},
38 | ),
39 | path("500/", handler500),
40 | ]
41 |
42 | if "robots" in settings.INSTALLED_APPS:
43 | urlpatterns.append(
44 | re_path(r"^robots\.txt", include("robots.urls")),
45 | )
46 |
47 | if "debug_toolbar" in settings.INSTALLED_APPS:
48 | import debug_toolbar
49 |
50 | urlpatterns = [path("__debug__/", include(debug_toolbar.urls))] + urlpatterns
51 |
52 |
53 | urlpatterns += [
54 | path("account/", include("[[project_name]].user.urls", namespace="user")),
55 | path("account/", include("allauth.urls")),
56 | path("admin/", admin.site.urls),
57 | path("hijack/", include("hijack.urls")),
58 | path("", include("[[project_name]].home.urls")),
59 | path("", include("django_components.urls")),
60 | ]
61 |
--------------------------------------------------------------------------------
/template/.pre-commit-config.yaml.jinja:
--------------------------------------------------------------------------------
1 | repos:
2 | ## system
3 | - repo: https://github.com/adamchainz/django-upgrade
4 | rev: 1.22.2
5 | hooks:
6 | - id: django-upgrade
7 | args: [--target-version, "5.1"]
8 |
9 | - repo: https://github.com/pre-commit/pre-commit-hooks
10 | rev: v5.0.0
11 | hooks:
12 | - id: check-yaml
13 | - id: check-merge-conflict
14 | - id: check-toml
15 | - id: trailing-whitespace
16 | - id: name-tests-test
17 | - id: debug-statements
18 | - id: mixed-line-ending
19 | - repo: https://github.com/pycqa/doc8
20 | rev: v1.1.2
21 | hooks:
22 | - id: doc8
23 |
24 | #python
25 | - repo: https://github.com/astral-sh/ruff-pre-commit
26 | rev: v0.9.1
27 | hooks:
28 | - id: ruff
29 | args: [--fix]
30 | - id: ruff-format
31 | - repo: https://github.com/astral-sh/uv-pre-commit
32 | rev: 0.5.18
33 | hooks:
34 | - id: uv-lock
35 |
36 | - repo: https://github.com/djlint/djLint
37 | rev: v1.36.4
38 | hooks:
39 | # TODO: Turn on when this works with django component and alpine.js
40 | # - id: djlint-reformat-django
41 | # files: "\\.html"
42 | - id: djlint-django
43 | files: "\\.html"
44 |
45 | # TODO: https://github.com/adamchainz/pre-commit-oxipng
46 |
47 |
48 | - repo: local
49 | hooks:
50 | - id: stylelint
51 | name: stylelint
52 | entry: npx stylelint
53 | language: node
54 | files: ^[[project_name]]/static_source/
55 | types_or: [css, scss]
56 | args: [--fix, --allow-empty-input, --cache]
57 |
58 | - id: eslint
59 | name: eslint
60 | entry: npx eslint
61 | language: node
62 | files: ^[[project_name]]/static_source/
63 | types_or: [javascript, ts]
64 | args: [--fix]
65 |
66 | - id: tsc
67 | name: tsc
68 | entry: npx tsc --noEmit
69 | files: ^[[project_name]]/static_source/
70 | language: system
71 | types_or: [javascript, ts]
72 | pass_filenames: false
73 |
--------------------------------------------------------------------------------
/template/[[project_name]]/static_source/js/forms/input.ts:
--------------------------------------------------------------------------------
1 | import AlpineInstance, { AlpineComponent } from "alpinejs";
2 | import inputListener from "./common";
3 |
4 | interface Input {
5 | //callback requires indexing to string and symbol
6 | [key: string]: unknown;
7 | [key: symbol]: unknown;
8 | //real types
9 | eventName: string;
10 | value: string | boolean;
11 | type: string;
12 | active: boolean;
13 | }
14 |
15 |
16 | const input = (...args: unknown[]): AlpineComponent => {
17 | const [eventName, value, type] = args as [string, string | boolean, string];
18 | return {
19 | eventName,
20 | value,
21 | type,
22 | active: false,
23 | init() {
24 | inputListener.call(this);
25 |
26 | if (this.type === 'checkbox') {
27 | // For checkboxes, initial state can come from either value or checked attribute
28 | const input = this.$el as HTMLInputElement;
29 | const isChecked = value == true || value === 'on' || input.hasAttribute('checked');
30 | this.value = isChecked ? 'on' : '';
31 | input.checked = isChecked;
32 |
33 | // Convert boolean true to 'on' for Django compatibility
34 | this.$watch('value', (newVal: string | boolean) => {
35 | const checked = newVal === true || newVal === 'on';
36 | input.checked = checked;
37 | this.value = checked ? 'on' : '';
38 | });
39 | // Also watch the input's checked state
40 | input.addEventListener('change', () => {
41 | this.value = input.checked ? 'on' : '';
42 | });
43 |
44 | } else if (this.value === "None") {
45 | this.value = "";
46 | } else if (this.$refs !== undefined && "input" in this.$refs) {
47 | // Toggle the focused state on an input when an initial value is set.
48 | this.active = !this.active;
49 | }
50 | if (this.eventName !== "input") {
51 | this.$watch("value", () => {
52 | this.$dispatch(
53 | this.eventName,
54 | { value: this.value },
55 | );
56 | });
57 | }
58 | },
59 | }
60 | }
61 | AlpineInstance.data("input", input);
62 |
--------------------------------------------------------------------------------
/copier.yml:
--------------------------------------------------------------------------------
1 | project_name:
2 | type: str
3 | help: >-
4 | Project name - used as both the Python package name and service name.
5 | Must be lowercase, start with a letter, and use only letters and underscores.
6 | Examples: my_project, awesome_django_app
7 | default: "sampleapp"
8 | validator: >-
9 | [% if not project_name %]
10 | Project name cannot be empty
11 | [% elif not (project_name | regex_search('^[a-z][a-z_]*[a-z]$')) %]
12 | Project name must be lowercase, start and end with a letter, and contain only letters and underscores
13 | [% elif '_-' in project_name or '-_' in project_name or '__' in project_name %]
14 | Project name cannot contain consecutive separators
15 | [% elif project_name | length > 50 %]
16 | Project name must be less than 50 characters
17 | [% endif %]
18 |
19 | project_name_verbose:
20 | type: str
21 | help: Human-friendly project name
22 | default: "[[ project_name | replace('_', ' ') | title ]]"
23 |
24 | author_name:
25 | type: str
26 | help: Author's name for pyproject and django admins
27 | default: Anonymous
28 |
29 | domain_name:
30 | type: str
31 | help: Production domain name
32 | default: example.com
33 |
34 | author_email:
35 | type: str
36 | help: Author's email for pyproject and django admins
37 | default: "[[ author_name | lower | replace(' ', '-') ]]@[[domain_name]]"
38 |
39 | description:
40 | type: str
41 | multiline: true
42 | help: A short description of the project
43 | default: A short description of the project.
44 |
45 | version:
46 | type: str
47 | help: Initial version number
48 | default: 0.1.0
49 |
50 | _subdirectory: template
51 |
52 | # Templates Customization
53 | _envops:
54 | block_end_string: "%]"
55 | block_start_string: "[%"
56 | comment_end_string: "#]"
57 | comment_start_string: "[#"
58 | keep_trailing_newline: true
59 | variable_end_string: "]]"
60 | variable_start_string: "[["
61 |
62 | _tasks:
63 | - "chmod +x manage.py"
64 | - "chmod +x scripts/*.sh"
65 | - "mise trust"
66 |
67 | _message_after_copy: |
68 | Project successfully created!
69 | For new projects, run `mise new-project` from the project root to:
70 | - setup database
71 | - install python packages
72 | - install node packages
73 |
--------------------------------------------------------------------------------
/template/scripts/create_patch.sh.jinja:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -euo pipefail
4 |
5 | RED='\033[0;31m'
6 | GREEN='\033[0;32m'
7 | YELLOW='\033[1;33m'
8 | NC='\033[0m'
9 |
10 | log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
11 | log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
12 | log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
13 |
14 | if ! [ -d .git ] || ! [ -d ../django-hydra ]; then
15 | log_error "Must be run from project root with template at ../django-hydra"
16 | exit 1
17 | fi
18 |
19 | if [ $# -eq 1 ]; then
20 | if ! [ "$(git cat-file -t "$1" 2>/dev/null)" = "commit" ]; then
21 | log_error "'$1' is not a valid git commit"
22 | exit 1
23 | fi
24 | PATCH_COMMIT=$1
25 | BASE_COMMIT=$(git rev-parse "$PATCH_COMMIT^1")
26 | elif [ $# -eq 2 ]; then
27 | PATCH_COMMIT=$1
28 | BASE_COMMIT=$2
29 | else
30 | PATCH_COMMIT=$(git rev-parse HEAD)
31 | BASE_COMMIT=$(git rev-parse "$PATCH_COMMIT^1")
32 | fi
33 |
34 | PROJECT_NAME="${PWD##*/}"
35 | WORK_DIR=$(mktemp -d)
36 | trap 'rm -rf "$WORK_DIR"' EXIT
37 |
38 | # Get original branch name
39 | ORIGINAL_BRANCH=$(cd ../django-hydra && git branch --show-current)
40 |
41 | # Extract commit message and patch
42 | COMMIT_MSG=$(git log -1 --format=%B "$PATCH_COMMIT")
43 | git format-patch --binary --minimal --stdout "$BASE_COMMIT..$PATCH_COMMIT" > "$WORK_DIR/changes.patch"
44 | sed -i.bak "s/$PROJECT_NAME/[[project_name]]/g" "$WORK_DIR/changes.patch"
45 |
46 | pushd "../django-hydra" >/dev/null
47 | TEMP_BRANCH="temp_patch_$(date +%s)"
48 | git checkout -b "$TEMP_BRANCH"
49 |
50 | if ! git apply -v --reject --directory="[[project_name]]" "$WORK_DIR/changes.patch" ; then
51 | log_warning "Merge conflicts detected. Resolve the conflicts then:"
52 | log_warning "1. git add changed files"
53 | log_warning "2. git commit -m '$COMMIT_MSG'"
54 | exit 1
55 | else
56 | git add .
57 | git commit -m "$COMMIT_MSG"
58 | fi
59 |
60 | git checkout "$ORIGINAL_BRANCH"
61 | if ! git merge "$TEMP_BRANCH" -m "Applied patch from instance project: $COMMIT_MSG"; then
62 | log_warning "Conflict during merge to $ORIGINAL_BRANCH. Resolve conflicts and merge manually."
63 | exit 1
64 | fi
65 | git branch -d "$TEMP_BRANCH"
66 | log_info "Successfully applied changes to template"
67 | popd >/dev/null
68 |
--------------------------------------------------------------------------------
/template/conftest.py:
--------------------------------------------------------------------------------
1 | import os
2 | from collections.abc import Generator
3 |
4 | import pytest
5 | from playwright.sync_api import BrowserContext, ConsoleMessage, Error, Page, Playwright, expect
6 |
7 |
8 | # See https://docs.pytest.org/en/7.1.x/reference/reference.html#pytest.hookspec.pytest_collection_modifyitems
9 | def pytest_collection_modifyitems(items: list[pytest.Item]) -> None:
10 | """
11 | Check if any tests are marked as integration and append the `vite` fixture to them if so.
12 | """
13 | for item in items:
14 | if item.get_closest_marker("integration"):
15 | item.fixturenames.append("vite") # type: ignore
16 |
17 |
18 | @pytest.fixture(scope="session")
19 | def vite() -> None:
20 | import platform
21 | import subprocess
22 | import sys
23 |
24 | completed_process = subprocess.run(
25 | ["npm", "run", "build"],
26 | check=False,
27 | shell=platform.system() == "Windows",
28 | )
29 | if completed_process.returncode != 0:
30 | print(completed_process.stderr)
31 | sys.exit(-1)
32 |
33 |
34 | @pytest.fixture(scope="session")
35 | def playwright(playwright: Playwright) -> Generator[Playwright]:
36 | """Override of playwright fixture so we can set up for use with Django.
37 |
38 | Background: https://github.com/microsoft/playwright-python/issues/439
39 | """
40 | os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
41 |
42 | yield playwright
43 |
44 |
45 | @pytest.fixture
46 | def context(context: BrowserContext) -> Generator[BrowserContext]:
47 | # Uncomment to disable or modify Playwright timeout
48 | context.set_default_timeout(1000) # 1 second in milliseconds
49 |
50 | yield context
51 |
52 |
53 | @pytest.fixture
54 | def page(page: Page) -> Generator[Page]:
55 | """Override of playwright page fixture that raises any console errors."""
56 | page.on("console", raise_error)
57 | page.set_default_timeout(1000) # For actions like click/fill
58 | expect.set_options(timeout=1000) # For assertions
59 | yield page
60 |
61 |
62 | def raise_error(msg: ConsoleMessage) -> None:
63 | """Raise an error if a console error occurs.
64 |
65 | Args:
66 | msg (ConsoleMessage): A console message.
67 | Raises:
68 | Error: An error message.
69 | """
70 | if msg.type != "error":
71 | return
72 |
73 | raise Error(f"error: {msg.text}, {msg.location['url']}")
74 |
--------------------------------------------------------------------------------
/template/[[project_name]]/templates/index.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% load heroicons %}
3 | {% block title %}
4 | Index
5 | {% endblock title %}
6 | {% block content %}
7 |
8 |
9 | Hello There!
10 |
11 |
{% include "samples/current_time.html" %}
12 | {% component "button"
13 | attrs:hx-get="{% url 'current_time' %}"
14 | attrs:hx-target="#current-time"
15 | attrs:hx-swap="innerHTML"
16 | attrs:hx-push-url="false" %}
17 | Refresh time
18 | {% endcomponent %}
19 | {% component "button"
20 | color="secondary"
21 | attrs:hx-get="{% url 'test_redirect' %}"
22 | attrs:hx-push-url="false" %}
23 | Test hx-redirect with messages
24 | {% endcomponent %}
25 | {% component "button"
26 | variant="outline"
27 | attrs:hx-get="{% url 'test_refresh' %}"
28 | %}
29 | Test hx-refresh with messages
30 | {% endcomponent %}
31 |
32 | {% component "popover" button_class="group flex items-center rounded-lg w-full p-1 hover:bg-gray-800/10" %}
33 | {% fill "trigger" %}
34 | {% component "button" %}
35 | open menu {% heroicon_micro "chevron-down" %}
36 | {% endcomponent %}
37 | {% endfill %}
38 | {% fill "content" %}
39 |
Menu Item One
40 |
Menu Item two
41 | {% endfill %}
42 | {% endcomponent %}
43 | {% component "modal" %}
44 | {% fill "title" %}
45 | welcome to hydra!
46 | {% endfill %}
47 | {% fill "content" %}
48 | This is a hydra modal
49 | {% endfill %}
50 | {% endcomponent %}
51 | {% component "tabs" %}
52 | {% component "tab_item" header="TabOne" %}
53 |
54 |
Testing 123
55 |
56 | {% endcomponent %}
57 | {% component "tab_item" header="TabTwo" %}
58 |
59 |
Testing 124
60 |
61 | {% endcomponent %}
62 | {% endcomponent %}
63 |
64 |
65 | {% endblock content %}
66 |
--------------------------------------------------------------------------------
/template/vite.config.mjs.jinja:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import { resolve } from 'path';
3 | import fg from 'fast-glob';
4 |
5 | export default defineConfig({
6 | // Where the project's files are
7 | root: resolve('./[[project_name]]/static_source/'),
8 | // The public path where assets are served, both in development and in production.
9 | base: "/static/",
10 | resolve: {
11 | alias: {
12 | // Use '@' in urls as a shortcut for './static_source'. (Currently used in CSS files.)
13 | '@': resolve('./[[project_name]]/static_source')
14 | },
15 | },
16 | build: {
17 | manifest: "manifest.json",
18 | rollupOptions: {
19 | input: {
20 | /* The bundle's entry point(s). If you provide an array of entry points or an object mapping names to
21 | entry points, they will be bundled to separate output chunks. */
22 | components: resolve(__dirname, './[[project_name]]/static_source/js/components.ts'),
23 | main: resolve(__dirname, './[[project_name]]/static_source/js/main.ts'),
24 | styles: resolve(__dirname, './[[project_name]]/static_source/css/styles.js'),
25 | admin: resolve(__dirname, './[[project_name]]/static_source/css/admin.js'),
26 | }
27 | },
28 | outDir: './', // puts the manifest.json in PROJECT_ROOT/static_source/ for Django to collect
29 | },
30 | plugins: [
31 | {
32 | name: 'watch-external', // https://stackoverflow.com/questions/63373804/rollup-watch-include-directory/63548394#63548394
33 | async buildStart() {
34 | const htmls = await fg(['[[project_name]]/**/*.html']);
35 | for (let file of htmls) {
36 | this.addWatchFile(file);
37 | }
38 | }
39 | },
40 | {
41 | name: 'reloadHtml',
42 | handleHotUpdate({ file, server }) {
43 | if (file.endsWith('.html')) {
44 | server.ws.send({
45 | type: 'custom',
46 | event: 'template-hmr',
47 | path: '*',
48 | });
49 | // returning an empty array prevents the hmr update from proceeding as normal
50 | return [];
51 | }
52 | },
53 | }
54 | ],
55 | });
56 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/modal/modal.html:
--------------------------------------------------------------------------------
1 | {% load heroicons %}
2 |
4 |
5 |
6 | {% slot "trigger" %}
7 | {% component "button" %}
8 | Open Dialog
9 | {% endcomponent %}
10 | {% endslot %}
11 |
12 |
13 |
17 |
18 |
21 |
22 |
23 |
27 |
28 | {% slot "close" %}
29 |
30 |
33 | Close modal
34 | {% heroicon_micro "x-mark" %}
35 |
36 |
37 | {% endslot %}
38 |
39 |
40 | {% slot "body" %}
41 |
42 |
43 | {% slot "title" %}
44 | {% endslot "title" %}
45 |
46 |
47 | {% slot "content" required default %}
48 |
49 |
Once published, your content will be visible to everyone.
50 |
51 | {% endslot %}
52 | {% endslot %}
53 |
54 |
55 |
56 | {% slot "footer" %}
57 | {% component "button" attrs:x-on:click="$dialog.close()" %}
58 | Ok
59 | {% endcomponent %}
60 | {% endslot %}
61 |
62 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/template/[[project_name]]/static_source/js/main.ts.jinja:
--------------------------------------------------------------------------------
1 | import focus from "@alpinejs/focus";
2 | import mask from "@alpinejs/mask";
3 | import ui from "@alpinejs/ui";
4 |
5 | import htmx from "htmx.org";
6 | import Alpine from "alpinejs";
7 | import Cookies from "js-cookie";
8 |
9 | import "./links.ts";
10 | import "./forms/input.ts";
11 | import "./forms/select.js";
12 | import "./forms/date_datetime.js";
13 |
14 | if (import.meta.env.MODE !== "development") {
15 | // // @ts-expect-error // this whole system is broken w/ vite
16 | // import("vite/modulepreload-polyfill"); // eslint-disable-line import/no-unresolved
17 | // https://github.com/vitejs/vite/issues/4786
18 | }
19 |
20 | // Turn off the history cache - have found this is generally error prone
21 | htmx.config.historyCacheSize = 0;
22 |
23 | htmx.defineExtension("get-csrf", {
24 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
25 | onEvent(name: string, evt: any): boolean {
26 | if (name === "htmx:configRequest") {
27 | evt.detail.headers["X-CSRFToken"] = Cookies.get(
28 | "[[project_name]]_csrftoken"
29 | );
30 | }
31 | return true
32 | },
33 | });
34 |
35 | htmx.defineExtension("get-timezone", {
36 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
37 | onEvent: function(name: string, evt: any): boolean {
38 | if (name === "htmx:configRequest") {
39 | evt.detail.headers["X-Timezone"] = Intl.DateTimeFormat().resolvedOptions().timeZone;
40 | }
41 | return true
42 | }
43 | });
44 |
45 | // This function will listen for HTMX errors and display the appropriate page
46 | // as needed. Without debug mode enabled, HTMX will normally refuse to
47 | // serve any HTML attached to an HTTP error code. This will allow us to present
48 | // users with custom error pages.
49 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
50 | htmx.on("htmx:beforeOnLoad", (event:any) => {
51 | const { xhr } = event.detail;
52 | if (xhr.status === 500 || xhr.status === 404) {
53 | event.stopPropagation();
54 | document.children[0].innerHTML = xhr.response;
55 | }
56 | });
57 |
58 | if (import.meta.hot) {
59 | import.meta.hot.on("template-hmr", () => {
60 | const dest = document.location.href;
61 | //switch to morph when ideomorph is ready
62 | htmx.ajax("get", dest, { target: "body" });
63 | });
64 | }
65 |
66 | window.Alpine = Alpine;
67 | Alpine.plugin(focus);
68 | Alpine.plugin(mask);
69 | Alpine.plugin(ui);
70 | Alpine.start();
71 |
--------------------------------------------------------------------------------
/template/[[project_name]]/static_source/img/loading.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
11 |
12 |
13 |
17 |
21 |
22 |
23 |
27 |
31 |
32 |
33 |
37 |
41 |
42 |
43 |
47 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/template/.vscode/launch.json.jinja:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "compounds": [
7 | {
8 | "name": "Debug [[ project_name_verbose ]]",
9 | "configurations": ["Django", "Vite", "Browser Debug"],
10 | "stopAll": true
11 | }
12 | ],
13 | "configurations": [
14 | {
15 | "name": "Django Command",
16 | "type": "python",
17 | "request": "launch",
18 | "program": "${workspaceFolder}/manage.py",
19 | "args": "${input:djangoManage}",
20 | "django": true,
21 | "justMyCode": true,
22 | "cwd": "${workspaceFolder}",
23 | "preLaunchTask": "poetryInstall"
24 | },
25 | {
26 | "name": "Django",
27 | "type": "python",
28 | "request": "launch",
29 | "program": "${workspaceFolder}/manage.py",
30 | "args": ["runserver"],
31 | "django": true,
32 | "justMyCode": true,
33 | "presentation": { "hidden": true },
34 | "cwd": "${workspaceFolder}",
35 | "preLaunchTask": "poetryInstall"
36 | // Disabled this in favor of "Browser Debug" as this doesn't stop automatically
37 | // https://github.com/microsoft/vscode/issues/163124
38 | //
39 | // "serverReadyAction": {
40 | // "pattern": ".*(https?:\\/\\/\\S+:[0-9]+\\/?).*",
41 | // "uriFormat": "%s",
42 | // "action": "debugWithChrome"
43 | // }
44 | },
45 | {
46 | "name": "Browser Debug",
47 | "type": "chrome",
48 | "request": "launch",
49 | "presentation": { "hidden": true },
50 | "url": "http://localhost:8000"
51 | },
52 | {
53 | "name": "Vite",
54 | "type": "node",
55 | "request": "launch",
56 | "runtimeExecutable": "npm",
57 | "runtimeArgs": ["run", "dev"],
58 | "presentation": { "hidden": true },
59 | "cwd": "${workspaceFolder}",
60 | "preLaunchTask": "npmInstall"
61 | },
62 | {
63 | "name": "Python: Debug Test",
64 | "type": "python",
65 | "request": "launch",
66 | "presentation": { "hidden": true },
67 | "program": "${file}",
68 | "purpose": ["debug-test"],
69 | "justMyCode": true,
70 | "preLaunchTask": "testSetup"
71 | }
72 | ],
73 | "inputs": [
74 | {
75 | "id": "djangoManage",
76 | "type": "promptString",
77 | "description": "Django admin commands.",
78 | "default": "migrate"
79 | }
80 | ]
81 | }
82 |
--------------------------------------------------------------------------------
/template/[[project_name]]/static_source/js/forms/common.ts:
--------------------------------------------------------------------------------
1 | declare global {
2 | interface Element {
3 | active: boolean;
4 | }
5 | }
6 |
7 | function getInput(component: Element): HTMLInputElement | HTMLTextAreaElement | null {
8 | if (component.matches("input, textarea")) {
9 | return component as HTMLInputElement;
10 | } else if (component.querySelector("input, textarea")) {
11 | return component.querySelector("input, textarea") as HTMLInputElement;
12 | }
13 | return null;
14 | }
15 |
16 | function updateFlag(this: HTMLElement & { $el: Element }, e: Event) {
17 | const currentInput: HTMLInputElement | HTMLTextAreaElement | null = getInput(this.$el);
18 |
19 | if (!currentInput) {
20 | console.warn("alpine input attached to something that isn't an input");
21 | return;
22 | }
23 |
24 | switch (e.type) {
25 | case "focus":
26 | case "input":
27 | this.active = true;
28 | break;
29 | case "blur":
30 | if (!currentInput.value) {
31 | this.active = false;
32 | } else {
33 | this.active = true;
34 | }
35 | break;
36 | default:
37 | }
38 |
39 | if (currentInput.classList.contains("autofilled")) {
40 | currentInput.classList.remove("autofilled");
41 | }
42 | };
43 |
44 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
45 | export default function inputListener(this:any) {
46 | const currentComponent = this.$el as HTMLInputElement;
47 | const currentInput = getInput(currentComponent);
48 | if (!currentInput) {
49 | return;
50 | }
51 |
52 | // Each of these events can have a different effect on the active state of the input.
53 | ["blur", "focus", "input"].forEach((eventName) => {
54 | currentInput.addEventListener(eventName, updateFlag.bind(this));
55 | });
56 |
57 | // We're using the animationstart event to detect when the browser has autofilled
58 | currentInput.addEventListener("animationstart", (e: Event) => {
59 | const { animationName } = e as AnimationEvent;
60 | if (!animationName) {
61 | return;
62 | }
63 | switch (animationName) {
64 | case "autofill-start":
65 | this.active = true;
66 | currentInput.classList.add("autofilled");
67 | break;
68 | case "autofill-cancel":
69 | if (document.activeElement === currentInput) {
70 | this.active = true;
71 | } else if (this.value === "") {
72 | this.active = false;
73 | }
74 | if (!currentInput.value) {
75 | currentInput.classList.remove("autofilled");
76 | }
77 | break;
78 | default:
79 | }
80 | });
81 | }
82 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/button/button.py:
--------------------------------------------------------------------------------
1 | from django_components import Component, register
2 |
3 |
4 | @register("button")
5 | class Button(Component):
6 | """
7 | Button component with HTMX support.
8 |
9 | Colors:
10 | - primary: Main action color
11 | - secondary: Less prominent actions
12 | Sizes:
13 | - sm: Small buttons (px-3 py-1.5 text-sm)
14 | - md: Medium buttons (px-4 py-2 text-base) [default]
15 | - lg: Large buttons (px-5 py-2.5 text-lg)
16 | Variants:
17 | - normal: Solid background color [default]
18 | - outline: Bordered with transparent background
19 | Slots:
20 | - content (required): Main button text
21 | - loading: Custom loading indicator for HTMX requests
22 |
23 | Usage:
24 | Basic:
25 | {% component "button" %}
26 | Click Me
27 | {% endcomponent %}
28 |
29 | With Icons:
30 | {% component "button" color="secondary" %}
31 | {% heroicon_mini "user" %}
32 | Profile
33 | {% heroicon_mini "arrow-right" %}
34 | {% endcomponent %}
35 |
36 | Outline Variant:
37 | {% component "button" variant="outline" %}
38 | Secondary Action
39 | {% endcomponent %}
40 |
41 | HTMX Integration:
42 | {% component "button"
43 | attrs:hx-post="{% url 'save'%}"
44 | attrs:hx-target="#result"
45 | %}
46 | Save
47 | {% endcomponent %}
48 | """
49 |
50 | template_name = "button.html"
51 |
52 | def get_context_data(
53 | self,
54 | variant: str = "normal",
55 | color: str = "primary",
56 | size: str = "md",
57 | disabled: bool = False,
58 | attrs: dict[str, str] | None = None,
59 | link: str = "",
60 | target: str = "",
61 | ):
62 | if attrs is None:
63 | attrs = {}
64 |
65 | # Build base classes
66 | classes = [
67 | "btn",
68 | f"btn-{size}",
69 | f"btn-{color}",
70 | ]
71 |
72 | if variant == "outline":
73 | classes.append("btn-outline")
74 |
75 | attrs["class"] = f"{' '.join(classes)} {attrs.get('class', '')}".strip()
76 |
77 | # Check for HTMX usage
78 | is_htmx = any(k.startswith("hx-") for k in attrs)
79 |
80 | # Handle disabled state
81 | if disabled:
82 | attrs["disabled"] = True
83 | attrs["aria-disabled"] = "true"
84 | attrs["class"] += " disabled"
85 |
86 | return {
87 | "attrs": attrs,
88 | "is_htmx": is_htmx,
89 | }
90 |
--------------------------------------------------------------------------------
/todo.txt:
--------------------------------------------------------------------------------
1 | fix silk/debug toolbar profiling
2 | https://github.com/django-commons/django-debug-toolbar/issues/1875
3 | https://github.com/jazzband/django-silk/issues/682
4 |
5 | swap cachealot for cacheops
6 |
7 | Make sure redis caching is configured bestly https://github.com/sebleier/django-redis-cache
8 | setup robots.txt and configure that to be part of django
9 | structlog
10 | Hijack
11 |
12 |
13 |
14 | **DJANGO
15 | set config up with
16 | https://github.com/rochacbruno/dynaconf
17 |
18 | Figure out html emails
19 | - https://github.com/peterbe/premailer
20 | - https://github.com/sunscrapers/django-templated-mail
21 |
22 | Get celery setup
23 | - https://github.com/pmclanahan/django-celery-email
24 |
25 | install
26 |
27 |
28 |
29 | next-boost looks nice
30 | https://github.com/rjyo/next-boost
31 |
32 |
33 | ** Documentation:
34 | Authentication
35 | - same subdomain cookie for same machine
36 | - domain cookie for different machines, same domain (must not be on https://publicsuffix.org/list/public_suffix_list.dat)
37 | - jwt for different domains.
38 |
39 |
40 |
41 | {{ foo:bar}} sets a dictionary in react code but is also a variable in jinja - look into changing jinja delimiter to something illegal in js
42 |
43 |
44 |
45 | documentation notes:
46 | Security
47 |
48 | - http only cookie
49 |
50 |
51 |
52 | mention that you need to use python3.8 to manage.py on prod
53 |
54 |
55 | Truly Static assets - where do they live
56 | - Django - easier to put a cdn in front of them.
57 | Get cdn configured with next and django
58 | https://nextjs.org/docs/api-reference/next.config.js/cdn-support-with-asset-prefix
59 |
60 | fix security issues from:
61 | https://securityheaders.com/?q=https%3A%2F%2Flightmatter-sampleapp.herokuapp.com%2F&followRedirects=on
62 |
63 | Better jest testing:
64 | https://github.com/testing-library/jest-dom
65 |
66 |
67 | cypress testing
68 | https://github.com/cypress-io/cypress-example-recipes
69 | - https://github.com/bahmutov/cypress-select-tests
70 | - get client side mock working w/ mirage, but not enabled when running tests through django
71 | - move nextjs server start into a once per class thing vs once per test in the django test case
72 | - figure out how to enforce new build of nextjs on running django tests
73 | - https://github.com/svish/cypress-hmr-restarter#readme for interactive testing
74 | - https://stackoverflow.com/questions/52231111/re-run-cypress-tests-in-gui-when-webpack-dev-server-causes-page-reload
75 |
76 |
77 | Sentry - Get consolidated error report for frontend and backend
78 | https://nextjs.org/docs/advanced-features/measuring-performance#sending-results-to-analytics
79 | https://github.com/zeit/next.js/discussions/12913 -- get error page working
80 |
--------------------------------------------------------------------------------
/template/[[project_name]]/static_source/js/test/links.test.ts:
--------------------------------------------------------------------------------
1 | import { beforeAll, describe, expect, it } from "vitest";
2 | import linksInit, { isCurrentPage, isExternalLink } from "../links";
3 |
4 | const mockAnchor = (href: string) => ({ href } as HTMLAnchorElement);
5 |
6 | describe("links", () => {
7 | beforeAll(() => {
8 | window.location.href = "https://test.com/example/";
9 | });
10 | it("isExternalLink", () => {
11 | // internal links
12 | expect(isExternalLink(mockAnchor(""))).toBe(false);
13 | expect(isExternalLink(mockAnchor("https://test.com/a#foo"))).toBe(false);
14 | expect(isExternalLink(mockAnchor("/newpage"))).toBe(false);
15 | expect(isExternalLink(mockAnchor("#anchor-ref"))).toBe(false);
16 | expect(isExternalLink(mockAnchor("?foo=bar"))).toBe(false);
17 | expect(isExternalLink(mockAnchor("tel:2013334444"))).toBe(false);
18 | expect(isExternalLink(mockAnchor("mailto:example@test.com"))).toBe(false);
19 | expect(isExternalLink(mockAnchor("https://test.com"))).toBe(false);
20 |
21 | // external links
22 | expect(isExternalLink(mockAnchor("https://example.com"))).toBe(true);
23 | expect(isExternalLink(mockAnchor("https://example.com/#anchor-ref"))).toBe(true);
24 | expect(isExternalLink(mockAnchor("ftp://test.com"))).toBe(true);
25 | });
26 |
27 | it("isCurrentPage", () => {
28 | // same page
29 | expect(isCurrentPage(mockAnchor("https://test.com/example/"))).toBe(true);
30 | expect(isCurrentPage(mockAnchor("/example/#anchor-ref"))).toBe(true);
31 | expect(isCurrentPage(mockAnchor("https://test.com/example/#anchor-ref"))).toBe(true);
32 | // diff page
33 | expect(isCurrentPage(mockAnchor("https://test.com"))).toBe(false); // needs trailing slash
34 | expect(isCurrentPage(mockAnchor("https://example.com/"))).toBe(false); // other domain
35 | expect(isCurrentPage(mockAnchor("https://test.com/otherpage"))).toBe(false); // other page on same domain
36 | });
37 |
38 | it("linksInit", () => {
39 | document.body.innerHTML = `
40 |
41 |
42 |
43 | `;
44 | const activeLink = document.getElementById("active") as HTMLAnchorElement;
45 | const externalLink = document.getElementById("external") as HTMLAnchorElement;
46 | const currentPageLink = document.getElementById("current") as HTMLAnchorElement;
47 |
48 | linksInit();
49 |
50 | expect(activeLink.classList.contains("active")).toBe(false);
51 | expect(externalLink.target).toBe("_blank");
52 | expect(currentPageLink.classList.contains("active")).toBe(true);
53 | expect(currentPageLink.classList.contains("active")).toBe(true);
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/template/[[project_name]]/home/forms.py.jinja:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.utils.translation import gettext_lazy as _
3 |
4 | from [[project_name]].util.widgets import ToggleWidget
5 |
6 |
7 | class TestForm(forms.Form):
8 | text_input = forms.CharField(
9 | label=_("Text Input"),
10 | help_text=_("This is a basic text input field"),
11 | widget=forms.TextInput(attrs={"placeholder": "this is a text input"}),
12 | required=False,
13 | )
14 |
15 | email_input = forms.EmailField(
16 | label=_("Email Input"),
17 | help_text=_("This is an email field"),
18 | required=False,
19 | )
20 |
21 | password_input = forms.CharField(
22 | label=_("Password Input"),
23 | widget=forms.PasswordInput,
24 | help_text=_("This is a password field"),
25 | required=False,
26 | )
27 |
28 | textarea = forms.CharField(
29 | label=_("Text Area"),
30 | widget=forms.Textarea,
31 | help_text=_("This is a textarea field"),
32 | required=False,
33 | )
34 |
35 | select = forms.ChoiceField(
36 | label=_("Select"),
37 | choices=[
38 | ("", "Select an option"),
39 | ("1", "Option 1"),
40 | ("2", "Option 2"),
41 | ("3", "Option 3"),
42 | ],
43 | help_text=_("This is a select field"),
44 | required=False,
45 | )
46 |
47 | radio = forms.ChoiceField(
48 | label=_("Radio"),
49 | widget=forms.RadioSelect,
50 | choices=[
51 | ("1", "Radio 1"),
52 | ("2", "Radio 2"),
53 | ("3", "Radio 3"),
54 | ],
55 | help_text=_("This is a radio field"),
56 | )
57 |
58 | checkbox = forms.BooleanField(label=_("Checkbox"), help_text=_("This is a checkbox field"))
59 |
60 | toggle = forms.BooleanField(
61 | label=_("Toggle"), required=False, widget=ToggleWidget, help_text=_("This is a toggle field")
62 | )
63 |
64 | date = forms.DateField(label=_("Date"), widget=forms.DateInput, help_text=_("This is a date field"))
65 |
66 | datetime = forms.DateTimeField(
67 | label=_("DateTime"), widget=forms.DateTimeInput, help_text=_("This is a datetime field")
68 | )
69 |
70 | favorite_colors = forms.MultipleChoiceField(
71 | label=_("Favorite Colors"),
72 | widget=forms.CheckboxSelectMultiple,
73 | choices=[
74 | ("red", "Red"),
75 | ("blue", "Blue"),
76 | ("green", "Green"),
77 | ("yellow", "Yellow"),
78 | ],
79 | help_text=_("Select your favorite colors"),
80 | required=True,
81 | error_messages={
82 | "required": _("Please select at least one color."),
83 | },
84 | )
85 |
--------------------------------------------------------------------------------
/docs/source/testing.rst:
--------------------------------------------------------------------------------
1 | Testing
2 | ========
3 |
4 | Testing the Template
5 | ---------------------
6 |
7 | To ensure that your template is working, you can run the :code:`test.sh` script.
8 | The :code:`test.sh` will do a run of the template, and then run the django tests and `prospector `_ against it.
9 |
10 | .. code-block:: console
11 |
12 | $ test.sh keepenv
13 |
14 | .. note::
15 | If you do not pass the argument keepenv, it will delete the old virtualenvironment. If you want to do this, simply run:
16 |
17 | .. code-block:: console
18 |
19 | $ test.sh
20 |
21 | Testing/Validation within your Project
22 | ---------------------------------------
23 |
24 |
25 | This will be run automatically when you attempt to commit code but if you want to manually validate/fix your code syntax during development you can run:
26 |
27 | .. code-block:: console
28 |
29 | $ poetry run pre-commit run --all-files
30 |
31 | This project uses the `pytest `_ framework with `pytest-django `_ enabling Django tests and `pytest-playwright `_ for end-to-end testing. This replaces the default Django tests using unittest.
32 |
33 | Django tests can be run by running:
34 |
35 | .. code-block:: console
36 |
37 | $ ./manage.py test
38 |
39 |
40 | .. warning::
41 | When doing one of the following, be sure to build Vite assets before running tests:
42 |
43 | * Initializing the project manually
44 | * Adding a new Vite asset (see :ref:`new_vite_assets`)
45 |
46 | To build the assets run:
47 |
48 | .. code-block:: console
49 |
50 | $ npm run build
51 |
52 | If you don't run this command before tests run, some tests may fail even if they would
53 | normally pass.
54 |
55 | Pytest
56 | ******
57 |
58 | While pytest is backwards-compatible with unittest, there are some key differences that implementers need to understand. If you're new to pytest in Django or playwright testing, reviewing the documentation for these libraries is well worth the time.
59 |
60 |
61 | Playwright
62 | **********
63 |
64 | `Playwright `_ allows for robust frontend testing across browser engines
65 |
66 | Of note is Playwright's `codegen `_ feature, which allows you to perform actions in the browser and have Playwright generate the code to perform those actions automatically.
67 |
68 | Rarely is codegen's generated code production ready immediately after recording, but it will get you most of the way through your end-to-end testing.
69 |
70 | `Coverage.py `_ can come in handy here in ensuring that the tests you write cover all of the code you write.
71 |
--------------------------------------------------------------------------------
/template/tailwind.config.js.jinja:
--------------------------------------------------------------------------------
1 | const plugin = require('tailwindcss/plugin');
2 |
3 | module.exports = {
4 | corePlugins: {
5 | preflight: false, //manually import this in app.css
6 | },
7 | content: [
8 | './[[project_name]]/static_source/**/*.html',
9 | './[[project_name]]/static_source/**/*.js',
10 | './[[project_name]]/static_source/**/*.scss',
11 | './[[project_name]]/static_source/**/*.sass',
12 | './[[project_name]]/templates/**/*.html',
13 | './[[project_name]]/components/**/*.html',
14 | './[[project_name]]/components/**/*.svg',
15 | './[[project_name]]/components/**/*.py',
16 |
17 | ],
18 | theme: {
19 | extend: {
20 | colors: {
21 | primary: {
22 | DEFAULT: "var(--primary)",
23 | focus: 'var(--primary-focus)',
24 | content: 'var(--primary-content)'
25 | },
26 | secondary: {
27 | DEFAULT: 'var(--secondary)',
28 | focus: 'var(--secondary-focus)',
29 | content: 'var(--secondary-content)'
30 | },
31 | accent: {
32 | DEFAULT: 'var(--accent)',
33 | 'focus': 'var(--accent-focus)',
34 | 'content': 'var(--accent-content)'
35 | },
36 | neutral: {
37 | '100': '#F3F6FA',
38 | '200': '#E5E7EB',
39 | '300': '#D1D5DB',
40 | '400': '#9CA3AF',
41 | '500': '#6B7280',
42 | '600': '#4B5563',
43 | '700': '#374151',
44 | '800': '#1F2937',
45 | '900': '#111827',
46 | },
47 | debug: {
48 | DEFAULT: 'var(--info)',
49 | 'focus': 'var(--info-focus)',
50 | 'content': 'var(--info-content)'
51 | },
52 | info: {
53 | DEFAULT: 'var(--info)',
54 | 'focus': 'var(--info-focus)',
55 | 'content': 'var(--info-content)'
56 | },
57 | success: {
58 | DEFAULT: 'var(--success)',
59 | 'focus': 'var(--success-focus)',
60 | 'content':'var(--success-content)',
61 | },
62 | warning: {
63 | DEFAULT: 'var(--warning)',
64 | 'focus': 'var(--warning-focus)',
65 | 'content': 'var(--warning-content)'
66 | },
67 | error: {
68 | DEFAULT: 'var(--error)',
69 | 'focus': 'var(--error-focus)',
70 | 'content': 'var(--error-content)'
71 | },
72 | }
73 | },
74 | },
75 | plugins: [
76 | plugin(function({ addVariant }) {
77 | // https://www.crocodile.dev/blog/css-transitions-with-tailwind-and-htmx
78 | addVariant('htmx-settling', ['&.htmx-settling', '.htmx-settling &']);
79 | addVariant('htmx-request', ['&.htmx-request', '.htmx-request &']);
80 | addVariant('htmx-swapping', ['&.htmx-swapping', '.htmx-swapping &']);
81 | addVariant('htmx-added', ['&.htmx-added', '.htmx-added &']);
82 | }),
83 | require('@tailwindcss/forms'),
84 | ],
85 | }
86 |
--------------------------------------------------------------------------------
/scripts/mac_intel_install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/zsh
2 | set -e
3 | # The purpose of the prefix is to give an indication of the point
4 | # of execution of this script, so that if something breaks it's easier
5 | # to see where that broke.
6 | prefix="[LM Install Script] "
7 |
8 | # Credit, Taken from: https://stackoverflow.com/a/34389425
9 | # Installs homebrew if it does not exist, or updates it if it does.
10 | which -s brew
11 | if [[ $? != 0 ]] ; then
12 | echo "${prefix}Installing homebrew"
13 | ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
14 | else
15 | echo "${prefix}Updating homebrew"
16 | brew update
17 | fi
18 |
19 | echo "${prefix}Installing node"
20 | brew install node
21 |
22 | echo "${prefix}Installing node version manager (nvm)"
23 | brew install nvm
24 |
25 | echo "${prefix}Installing pyenv"
26 | brew install pyenv
27 |
28 | echo "${prefix}Attemping to change pyenv version to 3.11.1"
29 | pyenv install 3.11.1
30 | pyenv global 3.11.1
31 |
32 | echo "${prefix}Installing git"
33 | brew install git
34 |
35 | echo "${prefix}Installing direnv"
36 | brew install direnv
37 |
38 | echo "${prefix}Installing postgres"
39 | brew install postgresql
40 |
41 | echo "${prefix}Installing libpq"
42 | brew install libpq
43 |
44 | echo "${prefix}Installing watchman"
45 | brew install watchman
46 |
47 | echo "${prefix}Adding pyenv, direnv, poetry and path config to .zshrc"
48 |
49 | echo "# START LM 3.0 Configuration" >> ~/.zshrc
50 | echo "NVM_DIR=~/.nvm" >> ~/.zshrc
51 | echo "eval \"source \$(brew --prefix nvm)/nvm.sh\"" >> ~/.zshrc
52 | echo "eval \"\$(pyenv init --path)\"" >> ~/.zshrc
53 | echo "eval \"\$(pyenv init -)\"" >> ~/.zshrc
54 | echo "eval \"\$(direnv hook zsh)\"" >> ~/.zshrc
55 | echo "export WORKON_HOME=\"~/.virtualenvs\"" >> ~/.zshrc
56 | echo "export PATH=\"\$HOME/.local/bin:\$PATH\"" >> ~/.zshrc
57 |
58 | echo "${prefix}Reloading zsh"
59 | source ~/.zshrc
60 |
61 | echo "${prefix}Attempting to change nvm version to v16.14.0(default)"
62 | nvm install v16.14.0
63 | nvm alias default v16.14.0
64 | nvm use default
65 |
66 | echo "${prefix}Attempting to install poetry"
67 | curl -sSL https://install.python-poetry.org | python3 -
68 |
69 | echo "# END LM 3.0 Configuration" >> ~/.zshrc
70 |
71 | echo "${prefix}Reloading zsh"
72 | source ~/.zshrc
73 |
74 | echo "${prefix}Creating default DB for postgres"
75 |
76 | db_name=$(whoami)
77 |
78 | if psql -lqt | cut -d \| -f 1 | grep -qw "${db_name}"; then
79 | echo "${prefix}Database ${db_name} already exists, skipping creation."
80 | else
81 | echo "${prefix}Database ${db_name} does not exist, creating."
82 | createdb "${db_name}"
83 | fi
84 |
85 | echo "Provided everything in this script executed without error"
86 | echo "You should now be setup"
87 | echo "You should check your ~/.zshrc"
88 | echo "Important: Now that you have completed the fresh machine setup, you should begin to setup the project."
89 | echo "If you are in an existing project, that is! You can do this with /scripts/setup_existing_project.sh"
90 |
--------------------------------------------------------------------------------
/template/[[project_name]]/components/svg/slack.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 | Slack
7 |
8 |
11 |
14 |
17 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/template/[[project_name]]/templates/header/mobile_menu.html:
--------------------------------------------------------------------------------
1 | {% if request %}
2 |
51 | {% endif %}
52 |
--------------------------------------------------------------------------------
/template/render.yaml.jinja:
--------------------------------------------------------------------------------
1 | previewsEnabled: true
2 | previewsExpireAfterDays: 5
3 | services:
4 | - type: web
5 | name: [[project_name]]
6 | env: python
7 | previewPlan: starter
8 | healthCheckPath: /
9 | buildCommand: "./build.sh"
10 | startCommand: poetry run gunicorn -c gunicorn.conf.py
11 | envVars:
12 | - key: PYTHON_VERSION
13 | value: 3.11.1
14 | - fromGroup: sentry
15 | - key: SECRET_KEY
16 | generateValue: true
17 | - key: DJANGO_SETTINGS_MODULE
18 | value: "[[project_name]].config.settings.prod"
19 | - key: DJANGO_ALLOWED_HOSTS
20 | fromService:
21 | type: web
22 | envVarKey: RENDER_EXTERNAL_HOSTNAME
23 | name: [[project_name]]
24 | - key: DJANGO_DEBUG
25 | value: false
26 | - key: DATABASE_URL
27 | fromDatabase:
28 | name: [[project_name]]-db
29 | property: connectionString
30 | - key: SENTRY_ENVIRONMENT
31 | fromService:
32 | type: web
33 | envVarKey: RENDER_SERVICE_NAME
34 | name: [[project_name]]
35 | - key: SENTRY_RELEASE
36 | fromService:
37 | type: web
38 | envVarKey: RENDER_GIT_COMMIT
39 | name: [[project_name]]
40 | - key: BUCKET_NAME
41 | value: [[project_name]]
42 | - key: AWS_ACCESS_KEY_ID
43 | fromService:
44 | type: web
45 | envVarKey: MINIO_ROOT_USER
46 | name: minio
47 | - key: AWS_SECRET_ACCESS_KEY
48 | fromService:
49 | type: web
50 | envVarKey: MINIO_ROOT_PASSWORD
51 | name: minio
52 | - key: AWS_ENDPOINT_URL_S3
53 | fromService:
54 | type: web
55 | envVarKey: RENDER_EXTERNAL_URL
56 | name: minio
57 | - key: REDIS_HOST
58 | fromService:
59 | name: redis
60 | type: pserv
61 | property: host # available properties are listed below
62 | - key: REDIS_PORT
63 | fromService:
64 | name: redis
65 | type: pserv
66 | property: port
67 |
68 | - type: web
69 | name: minio
70 | healthCheckPath: /minio/health/live
71 | env: docker
72 | dockerfilePath: ./compose/prod/minio/Dockerfile
73 | dockerContext: ./compose/prod/minio/
74 | disk:
75 | name: data
76 | mountPath: /data
77 | sizeGB: 10
78 | envVars:
79 | - key: MINIO_ROOT_USER
80 | generateValue: true
81 | - key: MINIO_ROOT_PASSWORD
82 | generateValue: true
83 | - key: PORT
84 | value: 9000
85 |
86 | - type: pserv
87 | name: redis
88 | dockerfilePath: ./compose/prod/redis/Dockerfile
89 | dockerContext: ./compose/prod/redis
90 | env: docker
91 | disk:
92 | name: data
93 | mountPath: /var/lib/redis
94 | sizeGB: 10
95 |
96 |
97 | databases:
98 | - name: [[project_name]]-db
99 | previewPlan: starter
100 | databaseName: [[project_name]] # optional (Render may add a suffix)
101 | ipAllowList: [] # optional (defaults to allow all)
102 |
103 | envVarGroups:
104 | - name: sentry
105 | envVars:
106 | - key: SENTRY_DSN
107 | sync: false
108 | - key: SENTRY_PROJECT
109 | value: [[project_name]]
110 |
--------------------------------------------------------------------------------
/template/.github/workflows/django_ci.yml.jinja:
--------------------------------------------------------------------------------
1 | name: Django CI
2 | on:
3 | pull_request:
4 | push:
5 | branches:
6 | - master
7 | - develop
8 |
9 | concurrency:
10 | group: ${{ github.workflow }}-${{ github.ref }}
11 | cancel-in-progress: true
12 |
13 | jobs:
14 | precommit:
15 | name: Precommit linting
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v4
19 | - uses: actions/cache@v4
20 | with:
21 | path: |
22 | ~/.cache/pre-commit
23 | node_modules
24 | key: precommit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml', '**/package-lock.json') }}
25 | restore-keys: |
26 | precommit-${{ runner.os }}-
27 |
28 | - uses: jdx/mise-action@v2
29 | - run: npm install
30 | - run: mise pre-commit --all-files --color=always ${{ inputs.extra_args }}
31 |
32 | test:
33 | name: Django CI
34 | runs-on: ubuntu-latest
35 | env:
36 | UV_CACHE_DIR: /tmp/.uv-cache
37 | MISE_ENV: ci
38 |
39 | steps:
40 | - uses: actions/checkout@v4
41 | - uses: actions/cache@v4
42 | with:
43 | path: |
44 | /tmp/.uv-cache
45 | ~/.cache/ms-playwright
46 | node_modules
47 | key: ${{ runner.os }}-deps-${{ hashFiles('**/uv.lock', '**/package-lock.json') }}
48 | restore-keys: |
49 | ${{ runner.os }}-deps-
50 | - uses: jdx/mise-action@v2
51 | - name: Install env
52 | run: |
53 | mise setup-js
54 | mise uv
55 | mise x -- playwright install chromium
56 |
57 | - name: Run tests
58 | run: mise test-all --cov
59 |
60 | - name: Minimize uv cache
61 | run: mise x -- uv cache prune --ci
62 |
63 |
64 | services:
65 | postgres:
66 | image: postgres:16-alpine
67 | env:
68 | POSTGRES_PASSWORD: postgres
69 |
70 | options: >-
71 | --health-cmd pg_isready
72 | --health-interval 5s
73 | --health-timeout 5s
74 | --health-retries 3
75 | ports:
76 | - 5432:5432
77 |
78 | deploy-master:
79 | name: Deploy to Production
80 | runs-on: ubuntu-latest
81 | if: github.ref == 'refs/heads/master'
82 | needs: [precommit, test]
83 | environment:
84 | name: production
85 | # url: # Optional: URL where your app is deployed
86 | concurrency: deploy-production
87 | steps:
88 | - uses: actions/checkout@v4
89 | - uses: superfly/flyctl-actions/setup-flyctl@master
90 | - run: flyctl deploy --remote-only
91 | env:
92 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
93 |
94 | deploy-develop:
95 | name: Deploy to Staging
96 | runs-on: ubuntu-latest
97 | if: github.ref == 'refs/heads/develop'
98 | needs: [precommit, test]
99 | environment:
100 | name: staging
101 | # url: # Optional: URL where your staging app is deployed
102 | concurrency: deploy-staging
103 | steps:
104 | - uses: actions/checkout@v4
105 | - uses: superfly/flyctl-actions/setup-flyctl@master
106 | - run: flyctl deploy --remote-only
107 | env:
108 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
109 |
110 |
111 | dependabot_approve:
112 | name: Auto Merge Dependabot prs
113 | if: ${{ github.actor == 'dependabot[bot]' }}
114 | runs-on: ubuntu-latest
115 |
116 | permissions:
117 | pull-requests: write
118 | contents: write
119 | needs: test
120 |
121 | steps:
122 | - uses: dependabot/fetch-metadata@v2
123 | - run: gh pr review --approve "$PR_URL" && gh pr merge --auto --squash "$PR_URL"
124 | env:
125 | PR_URL: ${{github.event.pull_request.html_url}}
126 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
127 |
--------------------------------------------------------------------------------
/template/scripts/export_project.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import html
3 | import os
4 | from fnmatch import fnmatch
5 | from pathlib import Path
6 |
7 | MANUAL_EXCLUDES = {
8 | "uv.lock",
9 | ".pytest_cache",
10 | ".coverage",
11 | ".ruff_cache",
12 | "dist",
13 | "build",
14 | "__pycache__",
15 | "*.pyc",
16 | "node_modules",
17 | "package-lock.json",
18 | ".venv",
19 | ".git",
20 | "static_source/assets/*",
21 | "*.svg",
22 | }
23 |
24 |
25 | def parse_gitignore(gitignore_path: Path) -> set[str]:
26 | if not gitignore_path.exists():
27 | return set()
28 |
29 | patterns = set()
30 | with open(gitignore_path) as f:
31 | for line in f:
32 | line = line.strip()
33 | if not line or line.startswith("#"):
34 | continue
35 |
36 | # Normalize pattern
37 | if line.startswith("/"):
38 | line = line[1:]
39 | if line.endswith("/"):
40 | patterns.add(f"{line}**")
41 | line = line[:-1]
42 |
43 | patterns.add(line)
44 | # Add pattern with and without leading **/ to catch both absolute and relative paths
45 | if not line.startswith("**/"):
46 | patterns.add(f"**/{line}")
47 |
48 | return patterns
49 |
50 |
51 | def should_include(path: Path, gitignore_patterns: set[str], source_root: Path) -> bool:
52 | try:
53 | rel_path = str(path.relative_to(source_root))
54 | except ValueError:
55 | return True
56 |
57 | # Check manual excludes first
58 | for pattern in MANUAL_EXCLUDES:
59 | if fnmatch(rel_path, pattern) or fnmatch(path.name, pattern):
60 | return False
61 |
62 | # Check gitignore patterns using full path
63 | for pattern in gitignore_patterns:
64 | if fnmatch(rel_path, pattern):
65 | return False
66 |
67 | return True
68 |
69 |
70 | def export_project(source_dir: str, output_file: str):
71 | source_path = Path(source_dir).resolve()
72 | gitignore_patterns = parse_gitignore(source_path / ".gitignore")
73 |
74 | with open(output_file, "w", encoding="utf-8") as f:
75 | f.write("\n")
76 |
77 | file_count = 0
78 | for root_dir, dirs, files in os.walk(source_path):
79 | root_path = Path(root_dir)
80 | dirs[:] = [d for d in dirs if should_include(root_path / d, gitignore_patterns, source_path)]
81 |
82 | for file in files:
83 | file_path = root_path / file
84 | if not should_include(file_path, gitignore_patterns, source_path):
85 | continue
86 |
87 | try:
88 | with open(file_path, encoding="utf-8") as src:
89 | content = src.read()
90 |
91 | relative_path = file_path.relative_to(source_path)
92 | f.write(f'\n')
93 | f.write(f"{html.escape(str(relative_path))} \n")
94 | f.write(
95 | f"{html.escape(content)} \n",
96 | )
97 | f.write(" \n")
98 | file_count += 1
99 |
100 | except UnicodeDecodeError:
101 | print(f"Skipping binary file: {file_path}")
102 | continue
103 |
104 | f.write(" ")
105 | print(f"Exported {file_count} files")
106 |
107 |
108 | if __name__ == "__main__":
109 | import argparse
110 |
111 | parser = argparse.ArgumentParser()
112 | parser.add_argument("source", help="Source directory")
113 | parser.add_argument("output", help="Output XML file")
114 | args = parser.parse_args()
115 |
116 | export_project(args.source, args.output)
117 |
--------------------------------------------------------------------------------
/scripts/export_project.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import html
3 | import os
4 | from fnmatch import fnmatch
5 | from pathlib import Path
6 |
7 | MANUAL_EXCLUDES = {
8 | "uv.lock",
9 | ".pytest_cache",
10 | ".coverage",
11 | ".ruff_cache",
12 | "dist",
13 | "build",
14 | "__pycache__",
15 | "*.pyc",
16 | "node_modules",
17 | "package-lock.json",
18 | ".venv",
19 | ".git",
20 | "static_source/assets/*",
21 | "*.svg",
22 | "docs/*",
23 | "todo.txt",
24 | }
25 |
26 |
27 | def parse_gitignore(gitignore_path: Path) -> set[str]:
28 | if not gitignore_path.exists():
29 | return set()
30 |
31 | patterns = set()
32 | with open(gitignore_path) as f:
33 | for line in f:
34 | line = line.strip()
35 | if not line or line.startswith("#"):
36 | continue
37 |
38 | # Normalize pattern
39 | if line.startswith("/"):
40 | line = line[1:]
41 | if line.endswith("/"):
42 | patterns.add(f"{line}**")
43 | line = line[:-1]
44 |
45 | patterns.add(line)
46 | # Add pattern with and without leading **/ to catch both absolute and relative paths
47 | if not line.startswith("**/"):
48 | patterns.add(f"**/{line}")
49 |
50 | return patterns
51 |
52 |
53 | def should_include(path: Path, gitignore_patterns: set[str], source_root: Path) -> bool:
54 | try:
55 | rel_path = str(path.relative_to(source_root))
56 | except ValueError:
57 | return True
58 |
59 | # Check manual excludes first
60 | for pattern in MANUAL_EXCLUDES:
61 | if fnmatch(rel_path, pattern) or fnmatch(path.name, pattern):
62 | return False
63 |
64 | # Check gitignore patterns using full path
65 | for pattern in gitignore_patterns:
66 | if fnmatch(rel_path, pattern):
67 | return False
68 |
69 | return True
70 |
71 |
72 | def export_project(source_dir: str, output_file: str):
73 | source_path = Path(source_dir).resolve()
74 | gitignore_patterns = parse_gitignore(source_path / ".gitignore")
75 |
76 | with open(output_file, "w", encoding="utf-8") as f:
77 | f.write("\n")
78 |
79 | file_count = 0
80 | for root_dir, dirs, files in os.walk(source_path):
81 | root_path = Path(root_dir)
82 | dirs[:] = [d for d in dirs if should_include(root_path / d, gitignore_patterns, source_path)]
83 |
84 | for file in files:
85 | file_path = root_path / file
86 | if not should_include(file_path, gitignore_patterns, source_path):
87 | continue
88 |
89 | try:
90 | with open(file_path, encoding="utf-8") as src:
91 | content = src.read()
92 |
93 | relative_path = file_path.relative_to(source_path)
94 | f.write(f'\n')
95 | f.write(f"{html.escape(str(relative_path))} \n")
96 | f.write(
97 | f"{html.escape(content)} \n",
98 | )
99 | f.write(" \n")
100 | file_count += 1
101 | print(f"included {file_path}")
102 |
103 | except UnicodeDecodeError:
104 | print(f"Skipping binary file: {file_path}")
105 | continue
106 |
107 | f.write(" ")
108 | print(f"Exported {file_count} files")
109 |
110 |
111 | if __name__ == "__main__":
112 | import argparse
113 |
114 | parser = argparse.ArgumentParser()
115 | parser.add_argument("source", help="Source directory")
116 | parser.add_argument("output", help="Output XML file")
117 | args = parser.parse_args()
118 |
119 | export_project(args.source, args.output)
120 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | static/
2 | *_flymake*
3 | *.\#*
4 | webpack-stats.json
5 |
6 |
7 | # Byte-compiled / optimized / DLL files
8 | __pycache__/
9 | *.py[cod]
10 |
11 | # C extensions
12 | *.so
13 |
14 | # Distribution / packaging
15 | .Python
16 | env/
17 | build/
18 | develop-eggs/
19 | dist/
20 | eggs/
21 | lib/
22 | lib64/
23 | parts/
24 | sdist/
25 | var/
26 | *.egg-info/
27 | .installed.cfg
28 | *.egg
29 |
30 | # environment variables
31 | .env
32 |
33 | # Installer logs
34 | pip-log.txt
35 | pip-delete-this-directory.txt
36 |
37 | # Unit test / coverage reports
38 | .tox/
39 | .coverage
40 | .cache
41 | nosetests.xml
42 | coverage.xml
43 | htmlcov/
44 |
45 |
46 | # Translations
47 | *.mo
48 |
49 | # Mr Developer
50 | .mr.developer.cfg
51 | .project
52 | .pydevproject
53 |
54 | # Rope
55 | .ropeproject
56 |
57 | # Django stuff:
58 | *.log
59 | *.pot
60 |
61 | # Sphinx documentation
62 | docs/_build/
63 |
64 |
65 | #django media folder
66 | media/
67 |
68 | #PYSCSS trash
69 | .sass-cache/
70 | *style.css
71 | *static_source/sass/style.css*
72 |
73 | #VIM
74 | *.swp
75 | ### vim ###
76 | [._]*.s[a-w][a-z]
77 | [._]s[a-w][a-z]
78 | *.un~
79 | Session.vim
80 | .netrwhist
81 | *~
82 |
83 |
84 | # Created by http://www.gitignore.io
85 |
86 | ### Emacs ###
87 | # -*- mode: gitignore; -*-
88 | *~
89 | \#*\#
90 | /.emacs.desktop
91 | /.emacs.desktop.lock
92 | *.elc
93 | auto-save-list
94 | tramp
95 | .\#*
96 |
97 | # Org-mode
98 | .org-id-locations
99 | *_archive
100 |
101 | # flymake-mode
102 | *_flymake.*
103 |
104 | # eshell files
105 | /eshell/history
106 | /eshell/lastdir
107 |
108 | # elpa packages
109 | /elpa/
110 |
111 | # reftex files
112 | *.rel
113 |
114 | # AUCTeX auto folder
115 | /auto/
116 |
117 |
118 | # Created by http://www.gitignore.io
119 |
120 | ### SublimeText ###
121 | # workspace files are user-specific
122 | *.sublime-workspace
123 |
124 | # project files should be checked into the repository, unless a significant
125 | # proportion of contributors will probably not be using SublimeText
126 | # *.sublime-project
127 |
128 | #sftp configuration file
129 | sftp-config.json
130 |
131 |
132 | # Created by http://www.gitignore.io
133 |
134 | ### OSX ###
135 | .DS_Store
136 | .AppleDouble
137 | .LSOverride
138 |
139 | # Icon must end with two \r
140 | Icon
141 |
142 | # mailhog
143 | /node_modules
144 | node/mailhog/mailhog
145 |
146 |
147 | # Thumbnails
148 | ._*
149 |
150 | # Files that might appear on external disk
151 | .Spotlight-V100
152 | .Trashes
153 |
154 | # Directories potentially created on remote AFP share
155 | .AppleDB
156 | .AppleDesktop
157 | Network Trash Folder
158 | Temporary Items
159 | .apdisk
160 |
161 |
162 | # Created by http://www.gitignore.io
163 |
164 | ### Linux ###
165 | *~
166 |
167 | # KDE directory preferences
168 | .directory
169 |
170 |
171 | ### PyCharm ###
172 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm
173 |
174 | *.iml
175 |
176 | ## Directory-based project format:
177 | .idea/
178 | # if you remove the above rule, at least ignore the following:
179 |
180 | # User-specific stuff:
181 | # .idea/workspace.xml
182 | # .idea/tasks.xml
183 | # .idea/dictionaries
184 |
185 | # Sensitive or high-churn files:
186 | # .idea/dataSources.ids
187 | # .idea/dataSources.xml
188 | # .idea/sqlDataSources.xml
189 | # .idea/dynamic.xml
190 | # .idea/uiDesigner.xml
191 |
192 | # Gradle:
193 | # .idea/gradle.xml
194 | # .idea/libraries
195 |
196 | # Mongo Explorer plugin:
197 | # .idea/mongoSettings.xml
198 |
199 | ## File-based project format:
200 | *.ipr
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 | # Crashlytics plugin (for Android Studio and IntelliJ)
215 | com_crashlytics_export_strings.xml
216 | crashlytics.properties
217 | crashlytics-build.properties
218 | testapp/
219 |
220 |
221 | # gunicorn pid file
222 | gunicorn.pid
223 | junit.xml
224 |
225 | #yarn stuf
226 | .yarn/*
227 | !.yarn/cache
228 | !.yarn/releases
229 | !.yarn/plugins
230 | !.yarn/sdks
231 | !.yarn/versions
232 |
233 | #project export for ai
234 | output.xml
235 |
236 | #stylelint cache
237 | .stylelintcache
238 |
239 |
240 | .mypy_cache/
241 | TAGS
242 |
--------------------------------------------------------------------------------