entirius-django-contact-forms 3.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- entirius_django_contact_forms-3.0.0/.gitattributes +2 -0
- entirius_django_contact_forms-3.0.0/.github/CODEOWNERS +3 -0
- entirius_django_contact_forms-3.0.0/.github/workflows/ci.yml +49 -0
- entirius_django_contact_forms-3.0.0/.github/workflows/publish.yml +18 -0
- entirius_django_contact_forms-3.0.0/.gitignore +33 -0
- entirius_django_contact_forms-3.0.0/.license-header.txt +3 -0
- entirius_django_contact_forms-3.0.0/.pre-commit-config.yaml +39 -0
- entirius_django_contact_forms-3.0.0/AGENTS.md +319 -0
- entirius_django_contact_forms-3.0.0/CLAUDE.md +1 -0
- entirius_django_contact_forms-3.0.0/LICENSE +373 -0
- entirius_django_contact_forms-3.0.0/Makefile +16 -0
- entirius_django_contact_forms-3.0.0/PKG-INFO +73 -0
- entirius_django_contact_forms-3.0.0/README.md +32 -0
- entirius_django_contact_forms-3.0.0/docs/erd-config.yaml +21 -0
- entirius_django_contact_forms-3.0.0/docs/multilang-emails.md +245 -0
- entirius_django_contact_forms-3.0.0/pyproject.toml +83 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/__init__.py +0 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/admin/__init__.py +12 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/admin/api_key.py +13 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/admin/booking.py +15 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/admin/booking_config.py +76 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/admin/channel.py +13 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/admin/contact_form.py +37 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/admin/contact_forms_settings.py +18 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/admin/form_notification_config.py +15 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/admin/form_type.py +16 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/admin/google_ads_config.py +38 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/admin/lead.py +33 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/admin/lead_creation_rule.py +15 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/admin/offline_conversion.py +51 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/__init__.py +0 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/admin/__init__.py +0 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/admin/pagination.py +12 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/admin/permissions.py +14 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/admin/urls.py +95 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/admin/views/__init__.py +0 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/admin/views/attachment_views.py +72 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/admin/views/booking_views.py +145 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/admin/views/channel_integrations_views.py +49 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/admin/views/form_type_views.py +61 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/admin/views/lead_views.py +182 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/admin/views/notification_config_views.py +204 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/admin/views/offline_conversion_views.py +138 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/admin/views/submission_views.py +194 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/public/__init__.py +0 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/public/authentication.py +50 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/public/booking_views.py +185 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/public/form_type_views.py +48 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/public/throttling.py +56 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/public/urls.py +33 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/public/views.py +206 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/apps.py +18 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/fixtures/django_contact_forms.cfg.yaml +187 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/management/__init__.py +0 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/management/commands/__init__.py +0 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/management/commands/forms-generate-api-key.py +39 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/migrations/0001_squashed_0014_alter_bookingtimewindow_id.py +681 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/migrations/__init__.py +0 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/__init__.py +39 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/api_key.py +42 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/base_model.py +17 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/booking.py +38 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/booking_config.py +145 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/booking_time_window.py +38 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/channel.py +33 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/contact_form.py +72 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/contact_form_attachment.py +59 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/contact_forms_settings.py +62 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/form_notification_config.py +49 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/form_type.py +52 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/google_ads_config.py +48 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/lead.py +88 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/lead_creation_rule.py +59 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/managers/__init__.py +0 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/managers/contact_form_attachment.py +13 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/offline_conversion.py +56 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/__init__.py +0 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/requests/__init__.py +0 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/requests/booking.py +38 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/requests/lead.py +22 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/requests/notification_config.py +48 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/requests/status.py +13 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/requests/submission.py +32 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/responses/__init__.py +0 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/responses/attachment.py +15 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/responses/booking.py +76 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/responses/channel_integrations.py +19 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/responses/form_type.py +17 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/responses/lead.py +50 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/responses/notification_config.py +29 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/responses/offline_conversion.py +32 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/responses/submission.py +36 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/__init__.py +0 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/booking_admin_service.py +76 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/booking_notification_service.py +89 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/booking_service.py +377 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/calendar/__init__.py +0 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/calendar/backends/__init__.py +0 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/calendar/backends/base.py +44 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/calendar/backends/google_calendar.py +56 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/calendar/backends/local_only.py +68 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/calendar/factory.py +53 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/calendar/fomo.py +411 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/channel_integrations_service.py +33 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/contact_form_service.py +143 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/conversions/__init__.py +3 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/conversions/google_ads_uploader.py +127 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/conversions/queue_service.py +129 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/exceptions.py +25 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/lead_creation_rule_service.py +103 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/lead_service.py +132 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/notification_service.py +196 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/settings.py +53 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/signals/__init__.py +3 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/signals/handlers.py +14 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/signals/lead_signals.py +15 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/tasks/__init__.py +0 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/tasks/send_contact_form_email.py +135 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/tasks/upload_pending_conversions.py +34 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/templates/base/footer.html +1 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/templates/base/header.html +5 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/urls.py +23 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/utils/__init__.py +0 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/utils/decorators.py +37 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/utils/encrypted_field.py +76 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/utils/encryption.py +47 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/utils/exceptions.py +15 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/utils/payloads.py +41 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/utils/v2_errors.py +49 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/utils/workers.py +32 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/views/__init__.py +0 -0
- entirius_django_contact_forms-3.0.0/src/django_contact_forms/views/views.py +122 -0
- entirius_django_contact_forms-3.0.0/tests/__init__.py +0 -0
- entirius_django_contact_forms-3.0.0/tests/conftest.py +15 -0
- entirius_django_contact_forms-3.0.0/tests/factories.py +193 -0
- entirius_django_contact_forms-3.0.0/tests/settings.py +92 -0
- entirius_django_contact_forms-3.0.0/tests/test_admin_api.py +423 -0
- entirius_django_contact_forms-3.0.0/tests/test_booking_time_window.py +75 -0
- entirius_django_contact_forms-3.0.0/tests/test_calendar_backends.py +217 -0
- entirius_django_contact_forms-3.0.0/tests/test_fomo.py +730 -0
- entirius_django_contact_forms-3.0.0/tests/test_form_types.py +112 -0
- entirius_django_contact_forms-3.0.0/tests/test_multilang_booking.py +226 -0
- entirius_django_contact_forms-3.0.0/tests/test_public_api.py +341 -0
- entirius_django_contact_forms-3.0.0/tests/test_service_booking.py +290 -0
- entirius_django_contact_forms-3.0.0/tests/test_service_conversions.py +153 -0
- entirius_django_contact_forms-3.0.0/tests/test_service_lead.py +118 -0
- entirius_django_contact_forms-3.0.0/tests/test_services.py +277 -0
- entirius_django_contact_forms-3.0.0/tests/test_v2_api_bookings.py +345 -0
- entirius_django_contact_forms-3.0.0/tests/test_v2_api_bookings_admin.py +169 -0
- entirius_django_contact_forms-3.0.0/tests/test_v2_api_channel_integrations.py +114 -0
- entirius_django_contact_forms-3.0.0/tests/test_v2_api_leads.py +151 -0
- entirius_django_contact_forms-3.0.0/tests/test_v2_api_offline_conversions.py +74 -0
- entirius_django_contact_forms-3.0.0/uv.lock +1896 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [develop]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [master, develop]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: astral-sh/setup-uv@v4
|
|
15
|
+
- run: uv sync --frozen --all-extras
|
|
16
|
+
- run: uv run ruff check .
|
|
17
|
+
- run: uv run ruff format --check .
|
|
18
|
+
- run: uv run pre-commit run insert-license --all-files
|
|
19
|
+
|
|
20
|
+
test:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
needs: lint
|
|
23
|
+
services:
|
|
24
|
+
postgres:
|
|
25
|
+
image: postgres:16
|
|
26
|
+
env:
|
|
27
|
+
POSTGRES_PASSWORD: postgres
|
|
28
|
+
ports: ['5432:5432']
|
|
29
|
+
options: >-
|
|
30
|
+
--health-cmd pg_isready --health-interval 5s --health-timeout 5s --health-retries 5
|
|
31
|
+
env:
|
|
32
|
+
DATABASE_URL: postgresql://postgres:postgres@localhost/test
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/checkout@v4
|
|
35
|
+
- uses: astral-sh/setup-uv@v4
|
|
36
|
+
- run: uv sync --frozen --all-extras
|
|
37
|
+
- run: uv run pytest -x -q
|
|
38
|
+
|
|
39
|
+
secret-scan:
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
steps:
|
|
42
|
+
- uses: actions/checkout@v4
|
|
43
|
+
with:
|
|
44
|
+
fetch-depth: 0
|
|
45
|
+
- name: gitleaks
|
|
46
|
+
run: |
|
|
47
|
+
V=v8.30.0 # keep equal to the gitleaks rev in .pre-commit-config.yaml
|
|
48
|
+
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/${V}/gitleaks_${V#v}_linux_x64.tar.gz" | sudo tar -xz -C /usr/local/bin gitleaks
|
|
49
|
+
gitleaks git --redact -v
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
environment: pypi # GH environment bound to the PyPI trusted publisher
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write # OIDC - short-lived PyPI token, no stored secrets
|
|
13
|
+
contents: read
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: astral-sh/setup-uv@v4
|
|
17
|
+
- run: uv build
|
|
18
|
+
- run: uv publish --trusted-publishing always
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
build/
|
|
6
|
+
dist/
|
|
7
|
+
|
|
8
|
+
# Virtualenv / uv
|
|
9
|
+
.venv/
|
|
10
|
+
.python-version
|
|
11
|
+
|
|
12
|
+
# Django (test project artifacts)
|
|
13
|
+
db.sqlite3
|
|
14
|
+
db.sqlite3-journal
|
|
15
|
+
*.log
|
|
16
|
+
|
|
17
|
+
# Test / coverage
|
|
18
|
+
.pytest_cache/
|
|
19
|
+
.coverage
|
|
20
|
+
htmlcov/
|
|
21
|
+
coverage.xml
|
|
22
|
+
|
|
23
|
+
# Tooling caches
|
|
24
|
+
.ruff_cache/
|
|
25
|
+
.mypy_cache/
|
|
26
|
+
|
|
27
|
+
# IDE / OS
|
|
28
|
+
.idea/
|
|
29
|
+
.vscode/
|
|
30
|
+
.DS_Store
|
|
31
|
+
|
|
32
|
+
# Secret scanning (forbidden-names blocklist - local symlink, never committed)
|
|
33
|
+
.gitleaks.toml
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
|
3
|
+
rev: v1.5.6
|
|
4
|
+
hooks:
|
|
5
|
+
- id: insert-license
|
|
6
|
+
files: \.py$
|
|
7
|
+
exclude: (^|/)__init__\.py$
|
|
8
|
+
args:
|
|
9
|
+
- --license-filepath
|
|
10
|
+
- .license-header.txt
|
|
11
|
+
- --comment-style
|
|
12
|
+
- "#"
|
|
13
|
+
|
|
14
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
15
|
+
rev: v0.15.20
|
|
16
|
+
hooks:
|
|
17
|
+
- id: ruff-check
|
|
18
|
+
args: [--fix]
|
|
19
|
+
- id: ruff-format
|
|
20
|
+
|
|
21
|
+
- repo: local
|
|
22
|
+
hooks:
|
|
23
|
+
- id: require-canonical-gitleaks-config
|
|
24
|
+
name: require canonical .gitleaks.toml (forbidden-names)
|
|
25
|
+
entry: bash -c 'grep -q "forbidden-names" .gitleaks.toml 2>/dev/null || { echo "Missing or non-canonical .gitleaks.toml - symlink the config per the internal secret-scanning standard"; exit 1; }'
|
|
26
|
+
language: system
|
|
27
|
+
always_run: true
|
|
28
|
+
pass_filenames: false
|
|
29
|
+
- id: gitleaks-config-untracked
|
|
30
|
+
name: .gitleaks.toml must stay gitignored
|
|
31
|
+
entry: bash -c '! git ls-files --error-unmatch .gitleaks.toml 2>/dev/null || { echo ".gitleaks.toml is tracked - add it to .gitignore"; exit 1; }'
|
|
32
|
+
language: system
|
|
33
|
+
always_run: true
|
|
34
|
+
pass_filenames: false
|
|
35
|
+
|
|
36
|
+
- repo: https://github.com/gitleaks/gitleaks
|
|
37
|
+
rev: v8.30.0
|
|
38
|
+
hooks:
|
|
39
|
+
- id: gitleaks
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Channel-scoped contact form submissions with file attachments, per-type email notification config,
|
|
4
|
+
booking calendar with optional Google Calendar backend, lead tracking with Google Ads offline-conversion
|
|
5
|
+
uploads, and async email dispatch for the Volkanos ecommerce platform — distribution
|
|
6
|
+
`entirius-django-contact-forms`, Django app `django_contact_forms`.
|
|
7
|
+
Dual API surface: v1 (legacy function-based) + v2 (DRF ViewSets, Pydantic, drf-spectacular).
|
|
8
|
+
|
|
9
|
+
**Tech:** Python >=3.11, Django >=4.0, DRF, Pydantic 2, drf-spectacular, Celery
|
|
10
|
+
(`contact_forms` + `contact_forms_conversions` queues)
|
|
11
|
+
|
|
12
|
+
## Commands
|
|
13
|
+
|
|
14
|
+
| Command | Meaning |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `make install` | sync dependencies (uv, incl. extras) |
|
|
17
|
+
| `make check` | lint + format-check (ruff) |
|
|
18
|
+
| `make fix` | auto-fix lint + format |
|
|
19
|
+
| `make test` | test suite (pytest) |
|
|
20
|
+
|
|
21
|
+
## Conventions
|
|
22
|
+
|
|
23
|
+
- English only: code, docs, commits, branches, PRs.
|
|
24
|
+
- MPL-2.0: every non-trivial source file carries the license header (pre-commit inserts it).
|
|
25
|
+
- Toolchain: uv + ruff + hatchling + pytest; all config in `pyproject.toml`; `uv.lock` committed.
|
|
26
|
+
- Git flow: `master` (production) + `develop` (integration); changes land via PR; semver tag on `master`.
|
|
27
|
+
- Never rename the Django app `django_contact_forms` — app_label and DB table prefix are a public schema contract.
|
|
28
|
+
- Default: do not commit — git is the user's call.
|
|
29
|
+
|
|
30
|
+
## Commit Message Format
|
|
31
|
+
|
|
32
|
+
**NEVER add `Co-Authored-By: Claude ...` (or any other Claude/Anthropic attribution) to commit messages.**
|
|
33
|
+
|
|
34
|
+
This overrides the default Claude Code behavior of appending a `Co-Authored-By` trailer. Commit messages MUST contain only the user's authored content — no robot footer, no "Generated with Claude Code" line, no co-author trailer.
|
|
35
|
+
|
|
36
|
+
Same rule applies to PR descriptions: no `Generated with [Claude Code]` footer.
|
|
37
|
+
|
|
38
|
+
## Architecture
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
src/django_contact_forms/
|
|
42
|
+
├── models/
|
|
43
|
+
│ ├── base_model.py # BaseModel (created_at, modified_at)
|
|
44
|
+
│ ├── channel.py # Channel (idx, label, admin_email, default_language)
|
|
45
|
+
│ ├── contact_form.py # ContactForm (12-digit string PK, JSON body, language FK)
|
|
46
|
+
│ ├── contact_form_attachment.py # ContactFormAttachment (FileField in PRIVATE_DIR)
|
|
47
|
+
│ ├── contact_forms_settings.py # ContactFormsSettings (bookings/google-ads feature toggles)
|
|
48
|
+
│ ├── api_key.py # APIKey (SHA256, FK→Channel)
|
|
49
|
+
│ ├── form_type.py # FormType (per-channel type catalog, is_default)
|
|
50
|
+
│ ├── form_notification_config.py # FormNotificationConfig (per type/slug email + client copy)
|
|
51
|
+
│ ├── booking_config.py # BookingConfig (per-channel calendar + FOMO settings)
|
|
52
|
+
│ ├── booking_time_window.py # BookingTimeWindow (≥1 working windows per config)
|
|
53
|
+
│ ├── booking.py # Booking (slot reservations)
|
|
54
|
+
│ ├── lead.py # Lead (FK→ContactForm, status pipeline)
|
|
55
|
+
│ ├── lead_creation_rule.py # LeadCreationRule (per channel/type/slug routing)
|
|
56
|
+
│ ├── google_ads_config.py # GoogleAdsConfig (OneToOne→Channel, encrypted refresh token)
|
|
57
|
+
│ ├── offline_conversion.py # OfflineConversionQueue (FK→Lead)
|
|
58
|
+
│ └── managers/
|
|
59
|
+
│
|
|
60
|
+
├── schemas/ # Pydantic requests/responses (v2)
|
|
61
|
+
├── services/
|
|
62
|
+
│ ├── contact_form_service.py # list, get, create submissions + get attachments
|
|
63
|
+
│ ├── notification_service.py # should_send_email (fallback chain), dispatch_email
|
|
64
|
+
│ ├── booking_service.py # slot grid, availability, create booking
|
|
65
|
+
│ ├── booking_admin_service.py # admin CRUD for booking configs
|
|
66
|
+
│ ├── booking_notification_service.py # booking e-mails (soft django-email import)
|
|
67
|
+
│ ├── lead_service.py # lead lifecycle, status transitions
|
|
68
|
+
│ ├── lead_creation_rule_service.py # rule resolution on submit
|
|
69
|
+
│ ├── channel_integrations_service.py # per-channel integration status
|
|
70
|
+
│ ├── calendar/ # backend factory: google_calendar (lazy SDK) / local_only
|
|
71
|
+
│ └── conversions/ # Google Ads uploader (lazy SDK), queue handling
|
|
72
|
+
│
|
|
73
|
+
├── api/
|
|
74
|
+
│ ├── admin/ # v2 Admin API (JWT + IsAdminUser)
|
|
75
|
+
│ └── public/ # v2 Public API (X-API-KEY auth)
|
|
76
|
+
│
|
|
77
|
+
├── views/views.py # v1 legacy: create_contact_form (unchanged)
|
|
78
|
+
├── tasks/
|
|
79
|
+
│ ├── send_contact_form_email.py # @shared_task(queue="contact_forms")
|
|
80
|
+
│ └── upload_pending_conversions.py # @shared_task(queue="contact_forms_conversions")
|
|
81
|
+
│
|
|
82
|
+
├── utils/ # decorators (v1 auth), payloads (marshmallow, v1),
|
|
83
|
+
│ # encrypted_field, v2_errors, workers
|
|
84
|
+
├── management/commands/ # forms-generate-api-key
|
|
85
|
+
├── admin/ # Django admin registrations
|
|
86
|
+
├── fixtures/ # Seed data (channels, API keys, configs, samples)
|
|
87
|
+
├── templates/django_contact_forms/email/
|
|
88
|
+
├── apps.py / settings.py / urls.py # v1 + v2 admin + v2 public URL includes
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Layer rule: `API → Services → Models → DB`. No business logic in views.
|
|
92
|
+
|
|
93
|
+
## Data Model
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
Channel (idx↑, label↑, admin_email, default_language FK→Language)
|
|
97
|
+
├── ContactForm (id[12-digit str PK], email, slug, type, code, body{JSON}, language FK)
|
|
98
|
+
│ └── ContactFormAttachment (name, attachment[FileField], FK→ContactForm)
|
|
99
|
+
├── APIKey (key[SHA256], FK→Channel)
|
|
100
|
+
├── FormType (code, label, is_default, FK→Channel)
|
|
101
|
+
│ UniqueConstraint(channel, code) + one is_default per channel
|
|
102
|
+
├── FormNotificationConfig (form_type, slug, send_email, send_client_copy, recipient_email)
|
|
103
|
+
│ UniqueConstraint(channel, form_type, slug)
|
|
104
|
+
├── BookingConfig (calendar backend, FOMO fields) ── BookingTimeWindow (order, start/end_time)
|
|
105
|
+
├── LeadCreationRule (channel/type/slug → create Lead)
|
|
106
|
+
└── GoogleAdsConfig (OneToOne, encrypted refresh token)
|
|
107
|
+
ContactForm ── Lead (status pipeline) ── OfflineConversionQueue (conversion upload rows)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
- `ContactForm.id` is a randomly generated 12-digit **string** PK — never auto-increment
|
|
111
|
+
- `FormType` is the per-channel catalog of allowed types (seeded via fixtures, e.g.
|
|
112
|
+
general/product/default). Submit resolves an incoming type against it; empty/unknown
|
|
113
|
+
falls back to the channel's `is_default` row, then `CONTACT_FORM_DEFAULT_TYPE`.
|
|
114
|
+
- `FormNotificationConfig` controls per-type/slug email behavior with fallback chain:
|
|
115
|
+
exact(channel,type,slug) → type-level(channel,type,"") → channel-level(channel,"","") → global setting.
|
|
116
|
+
`send_email` gates the admin notification; `send_client_copy` gates the submitter copy
|
|
117
|
+
(additionally requires `body.send_copy` truthy on the submission).
|
|
118
|
+
|
|
119
|
+
## API v2 Endpoints
|
|
120
|
+
|
|
121
|
+
### Admin (JWT + IsAdminUser)
|
|
122
|
+
|
|
123
|
+
URL prefix: `api/contact-forms/v2/admin/`
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
GET submissions/ List (paginated, filterable)
|
|
127
|
+
GET submissions/<str:pk>/ Retrieve (with body + attachments)
|
|
128
|
+
GET submissions/<pk>/attachments/<id>/download/ Download file
|
|
129
|
+
GET form-types/?channel_idx=<idx> List a channel's form types (CMS selector source)
|
|
130
|
+
GET notifications/ List notification configs
|
|
131
|
+
POST notifications/ Create config
|
|
132
|
+
GET notifications/<int:pk>/ Retrieve config
|
|
133
|
+
PATCH notifications/<int:pk>/ Update config
|
|
134
|
+
DELETE notifications/<int:pk>/ Delete config
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Filters on submissions: `type`, `slug`, `channel` (query params), `search` (id/email/code).
|
|
138
|
+
Ordering: `created_at`, `-created_at`, `email`, `-email`, `type`, `-type`.
|
|
139
|
+
|
|
140
|
+
### Public (X-API-KEY)
|
|
141
|
+
|
|
142
|
+
URL prefix: `api/contact-forms/v2/`
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
GET <channel_idx>/form-types/ List the channel's form types (throttled)
|
|
146
|
+
POST <channel_idx>/submit/ Submit form
|
|
147
|
+
POST <channel_idx>/submit/<type_id>/ Submit form with explicit type (legacy URL)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Supports multipart/form-data for file uploads. Body can be JSON object or string.
|
|
151
|
+
|
|
152
|
+
**Form type** comes from (priority): the `form_type` request field → `body.form_type`
|
|
153
|
+
→ legacy `body.type` → URL `type_id`. The value is normalized against the channel's
|
|
154
|
+
`FormType` catalog; empty/unknown resolves to the channel default. **Client copy:** set
|
|
155
|
+
`body.send_copy=true` and a matching `FormNotificationConfig.send_client_copy=true` to
|
|
156
|
+
have the submitter receive a confirmation copy. Submit is rate-limited
|
|
157
|
+
(`AnonRateThrottle`, scope `contact_forms_submit`).
|
|
158
|
+
|
|
159
|
+
## API v1 Endpoints (Legacy)
|
|
160
|
+
|
|
161
|
+
URL prefix: `api/contact/<version>/<channel_idx>/`
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
POST contact_form/ Submit (no type)
|
|
165
|
+
POST contact_form/<type_id>/ Submit with type
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Auth:** `X-API-KEY` via `@channel_view` decorator. Marshmallow DTOs. Unchanged.
|
|
169
|
+
|
|
170
|
+
## Notification Config
|
|
171
|
+
|
|
172
|
+
`FormNotificationConfig` controls whether email is sent and to whom for each form submission.
|
|
173
|
+
|
|
174
|
+
**Fallback chain** (one walk, shared by `should_send_email` and `should_send_client_copy`):
|
|
175
|
+
1. Exact match: `(channel, form_type, slug)`
|
|
176
|
+
2. Type-level: `(channel, form_type, "")`
|
|
177
|
+
3. Channel-level: `(channel, "", "")`
|
|
178
|
+
4. Global: `CONTACT_FORM_SEND_ADMIN_EMAIL` / `CONTACT_FORM_SEND_CLIENT_COPY` setting
|
|
179
|
+
|
|
180
|
+
`recipient_email` override: if set on the matching config, overrides `channel.admin_email`.
|
|
181
|
+
|
|
182
|
+
**Client copy:** `send_client_copy` on the matching config, AND `body.send_copy` truthy on
|
|
183
|
+
the submission, AND a non-empty submitter email → the submitter receives a copy rendered by
|
|
184
|
+
django-email's `ContactFormClientCopyEmail` (separate template/subject from the admin one).
|
|
185
|
+
|
|
186
|
+
**Editable fields** via `update_config`: `form_type`, `slug`, `send_email`,
|
|
187
|
+
`send_client_copy`, `recipient_email` (whitelisted — anything else raises `ValueError`).
|
|
188
|
+
|
|
189
|
+
## Multilang Emails
|
|
190
|
+
|
|
191
|
+
All templates, translations, and rendering live in **django-email** (`email` extra). This
|
|
192
|
+
module stores the `ContactForm.language` FK and resolves it to an ISO2
|
|
193
|
+
code; the actual render happens inside `django-email EmailService`
|
|
194
|
+
subclasses.
|
|
195
|
+
|
|
196
|
+
| Email | Language source | Subclass |
|
|
197
|
+
|---|---|---|
|
|
198
|
+
| Booker confirmation | `ContactForm.language` → `channel.default_language` | `BookingConfirmationEmail` |
|
|
199
|
+
| Booking admin notification | `channel.default_language` | `BookingAdminNotificationEmail` |
|
|
200
|
+
| Generic form notification | `channel.default_language` | `ContactFormSubmissionEmail` |
|
|
201
|
+
| Client copy (to submitter) | `ContactForm.language` → `channel.default_language` | `ContactFormClientCopyEmail` |
|
|
202
|
+
|
|
203
|
+
Full pattern + "how to add a new translated email": [docs/multilang-emails.md](docs/multilang-emails.md).
|
|
204
|
+
|
|
205
|
+
## Multi-window working hours
|
|
206
|
+
|
|
207
|
+
Working hours per channel are defined as ≥1 ``BookingTimeWindow`` rows linked to
|
|
208
|
+
``BookingConfig`` (FK with ``related_name="time_windows"``). Each window has
|
|
209
|
+
``order``, ``start_time``, ``end_time`` (both ``TimeField`` — minute precision).
|
|
210
|
+
Slot generation iterates each window independently; clusters and synthetic FOMO
|
|
211
|
+
busy periods are constrained per-window (never span boundaries).
|
|
212
|
+
Admin edits windows via the inline editor on ``BookingConfigAdmin``
|
|
213
|
+
(e.g., 10:00-12:30 + 16:00-17:00).
|
|
214
|
+
|
|
215
|
+
API ``Slot`` shape unchanged — public ``/bookings/slots/`` endpoint emits a flat
|
|
216
|
+
list per day; multi-window structure lives only in the config layer.
|
|
217
|
+
|
|
218
|
+
## FOMO synthetic busy slots
|
|
219
|
+
|
|
220
|
+
Cosmetic layer that adds fake busy slots on top of real Google Calendar busy
|
|
221
|
+
intervals so a young or low-traffic booking calendar doesn't look empty (which
|
|
222
|
+
itself deters visitors). Three layers in
|
|
223
|
+
[`services/calendar/fomo.py`](src/django_contact_forms/services/calendar/fomo.py):
|
|
224
|
+
|
|
225
|
+
1. **Time-decay** — today ×1.5 intensity, tomorrow ×1.3, days 2-6 ×1.0,
|
|
226
|
+
days 7+ ×0.75. Capped at 80% of slots blocked per day.
|
|
227
|
+
2. **Slot clusters** — 2-4 consecutive synthetic-busy slots per cluster, weighted
|
|
228
|
+
toward business peak hours (10-12, 13-16). Mirrors how real bookings cluster.
|
|
229
|
+
3. **Day blackouts** — 0-3 full-day blocks within the visible window. Today and
|
|
230
|
+
tomorrow are always excluded so "fresh availability" urgency works.
|
|
231
|
+
|
|
232
|
+
Per-day RNG seed = `HMAC-SHA256(SECRET_KEY, channel.idx + day.isoformat())` so the
|
|
233
|
+
layout is stable across page refreshes within a calendar day and rotates
|
|
234
|
+
naturally at midnight. The HMAC construction (rather than a raw `sha256(...)`
|
|
235
|
+
concatenation) is what gives us the SECRET_KEY-keyed determinism; without it,
|
|
236
|
+
anyone scraping `channel.idx` could replay the layout.
|
|
237
|
+
|
|
238
|
+
**Three `BookingConfig` fields** (Django admin only — no CMS panel):
|
|
239
|
+
|
|
240
|
+
| Field | Default | Range |
|
|
241
|
+
|---|---|---|
|
|
242
|
+
| `fomo_enabled` | `False` | bool |
|
|
243
|
+
| `fomo_intensity` | `50` | 0-100 (0=off, 50=balanced, 80=aggressive) |
|
|
244
|
+
| `fomo_blackout_full_days` | `1` | 0-3 |
|
|
245
|
+
|
|
246
|
+
**Integration**: `booking_service.get_available_slots` merges synthetic into the
|
|
247
|
+
real busy list before slot grid generation. `is_slot_free` (race-condition guard
|
|
248
|
+
before `create_event`) consults only the real backend — synthetic intervals
|
|
249
|
+
**never** block an actual booking, only filter visibility. The API response shape
|
|
250
|
+
is unchanged (`available: bool`); no synthetic flag leaks to clients.
|
|
251
|
+
|
|
252
|
+
Disable per-channel by toggling `fomo_enabled=False`. Disable globally by setting
|
|
253
|
+
`fomo_intensity=0` for every config (or leaving the master toggle off).
|
|
254
|
+
|
|
255
|
+
## Celery Tasks
|
|
256
|
+
|
|
257
|
+
**`send_contact_form_email`** — Queue: `contact_forms`. Thin async wrapper
|
|
258
|
+
around django-email service classes. `send_admin` (default True) sends the
|
|
259
|
+
admin notification via `ContactFormSubmissionEmail`; `send_client_copy`
|
|
260
|
+
(default False) additionally sends the submitter copy via
|
|
261
|
+
`ContactFormClientCopyEmail`. Controlled by `notification_service` in v2, by
|
|
262
|
+
`CONTACT_FORM_SEND_ADMIN_EMAIL` in v1. Deprecated kwargs
|
|
263
|
+
(`template_html_path`, `subject`, `extra_context`) remain in the signature
|
|
264
|
+
for backward compatibility with previously enqueued tasks but are ignored.
|
|
265
|
+
|
|
266
|
+
**`upload_pending_conversions`** — Queue: `contact_forms_conversions`. Batch-uploads
|
|
267
|
+
queued `OfflineConversionQueue` rows via the `google-ads` extra (lazy import);
|
|
268
|
+
retries with backoff (`max_retries=3`).
|
|
269
|
+
|
|
270
|
+
## Settings Reference
|
|
271
|
+
|
|
272
|
+
| Setting | Default | Description |
|
|
273
|
+
|---------|---------|-------------|
|
|
274
|
+
| `PRIVATE_DIR` | **required** | Root for private file storage |
|
|
275
|
+
| `FORMS_MAX_JSON_MB_SIZE` | `50` | Max JSON body size in MB |
|
|
276
|
+
| `FORMS_MAX_ATTACHMENT_MB_SIZE` | `50` | Max total attachment size per form |
|
|
277
|
+
| `FORMS_MAX_ATTACHMENT_COUNT` | `10` | Max attachments per form |
|
|
278
|
+
| `DEFAULT_FROM_EMAIL_CONTACT_FORM` | `None` | From address for emails |
|
|
279
|
+
| `CONTACT_FORM_SEND_ADMIN_EMAIL` | `True` | Global admin-email toggle (v1 + v2 fallback) |
|
|
280
|
+
| `CONTACT_FORM_SEND_CLIENT_COPY` | `False` | Global client-copy fallback (per-config overrides) |
|
|
281
|
+
| `CONTACT_FORM_DEFAULT_TYPE` | `"default"` | Type code used when no FormType matches / is_default |
|
|
282
|
+
| `GOOGLE_ADS_DEVELOPER_TOKEN` | `""` | Google Ads global developer token (`google-ads` extra) |
|
|
283
|
+
| `GOOGLE_ADS_OAUTH_CLIENT_ID` / `_SECRET` | `""` | OAuth app credentials (`google-ads` extra) |
|
|
284
|
+
| `CONTACT_FORMS_CONVERSIONS_BATCH_SIZE` | `100` | Conversion upload batch size |
|
|
285
|
+
| `CONTACT_FORMS_CREDENTIALS_DIR` | `"/app/credentials/"` | Service-account JSON dir (`bookings` extra) |
|
|
286
|
+
|
|
287
|
+
## Testing
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
uv run pytest # postgres via DATABASE_URL (default: postgres:postgres@localhost:5432/test)
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
| File | Scope |
|
|
294
|
+
|------|-------|
|
|
295
|
+
| `test_admin_api.py` | Auth (401/403/200), list, retrieve, search, pagination, notification config CRUD |
|
|
296
|
+
| `test_public_api.py` | Auth (401 no key / wrong key, 201 valid), submit, validation, multipart upload |
|
|
297
|
+
| `test_services.py` | contact_form_service CRUD, notification_service fallback chain |
|
|
298
|
+
| `test_service_booking.py` / `test_booking_time_window.py` / `test_fomo.py` | booking domain |
|
|
299
|
+
| `test_service_lead.py` / `test_service_conversions.py` | leads + conversion uploads |
|
|
300
|
+
| `test_form_types.py` / `test_multilang_booking.py` / `test_calendar_backends.py` | type catalog, i18n, backends |
|
|
301
|
+
|
|
302
|
+
## Dependencies
|
|
303
|
+
|
|
304
|
+
**Runtime:** Django >=4.0, celery, DRF >=3.14, djangorestframework-simplejwt >=5.3, pydantic >=2.0,
|
|
305
|
+
drf-spectacular >=0.27, cryptography, marshmallow (v1 only), django-admin-inline-paginator
|
|
306
|
+
|
|
307
|
+
**Cross-module:** entirius-django-regional, entirius-django-utils, entirius-py-idx-normalizator,
|
|
308
|
+
entirius-py-process-logger; extras: `bookings` (entirius-py-google-calendar-sdk),
|
|
309
|
+
`google-ads` (entirius-py-google-ads-sdk), `email` (entirius-django-email)
|
|
310
|
+
|
|
311
|
+
## Gotchas
|
|
312
|
+
|
|
313
|
+
- `PRIVATE_DIR` raises `OSError` at import time if unset
|
|
314
|
+
- `ContactForm.id` is a 12-digit random **string** PK, not an integer
|
|
315
|
+
- Attachments stored in `CUSTOM_FORM_ATTACHMENT_DIR` outside `MEDIA_ROOT` — require download view
|
|
316
|
+
- v1 and v2 coexist: v1 uses marshmallow + function views, v2 uses Pydantic + DRF ViewSets
|
|
317
|
+
- Public v2 API returns `(AnonymousUser, channel)` from auth — access channel via `request.auth`
|
|
318
|
+
- Calendar/Ads SDK imports are lazy behind the `bookings` / `google-ads` extras; without them the
|
|
319
|
+
local-only backend and a no-op uploader are used
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|