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.
Files changed (153) hide show
  1. entirius_django_contact_forms-3.0.0/.gitattributes +2 -0
  2. entirius_django_contact_forms-3.0.0/.github/CODEOWNERS +3 -0
  3. entirius_django_contact_forms-3.0.0/.github/workflows/ci.yml +49 -0
  4. entirius_django_contact_forms-3.0.0/.github/workflows/publish.yml +18 -0
  5. entirius_django_contact_forms-3.0.0/.gitignore +33 -0
  6. entirius_django_contact_forms-3.0.0/.license-header.txt +3 -0
  7. entirius_django_contact_forms-3.0.0/.pre-commit-config.yaml +39 -0
  8. entirius_django_contact_forms-3.0.0/AGENTS.md +319 -0
  9. entirius_django_contact_forms-3.0.0/CLAUDE.md +1 -0
  10. entirius_django_contact_forms-3.0.0/LICENSE +373 -0
  11. entirius_django_contact_forms-3.0.0/Makefile +16 -0
  12. entirius_django_contact_forms-3.0.0/PKG-INFO +73 -0
  13. entirius_django_contact_forms-3.0.0/README.md +32 -0
  14. entirius_django_contact_forms-3.0.0/docs/erd-config.yaml +21 -0
  15. entirius_django_contact_forms-3.0.0/docs/multilang-emails.md +245 -0
  16. entirius_django_contact_forms-3.0.0/pyproject.toml +83 -0
  17. entirius_django_contact_forms-3.0.0/src/django_contact_forms/__init__.py +0 -0
  18. entirius_django_contact_forms-3.0.0/src/django_contact_forms/admin/__init__.py +12 -0
  19. entirius_django_contact_forms-3.0.0/src/django_contact_forms/admin/api_key.py +13 -0
  20. entirius_django_contact_forms-3.0.0/src/django_contact_forms/admin/booking.py +15 -0
  21. entirius_django_contact_forms-3.0.0/src/django_contact_forms/admin/booking_config.py +76 -0
  22. entirius_django_contact_forms-3.0.0/src/django_contact_forms/admin/channel.py +13 -0
  23. entirius_django_contact_forms-3.0.0/src/django_contact_forms/admin/contact_form.py +37 -0
  24. entirius_django_contact_forms-3.0.0/src/django_contact_forms/admin/contact_forms_settings.py +18 -0
  25. entirius_django_contact_forms-3.0.0/src/django_contact_forms/admin/form_notification_config.py +15 -0
  26. entirius_django_contact_forms-3.0.0/src/django_contact_forms/admin/form_type.py +16 -0
  27. entirius_django_contact_forms-3.0.0/src/django_contact_forms/admin/google_ads_config.py +38 -0
  28. entirius_django_contact_forms-3.0.0/src/django_contact_forms/admin/lead.py +33 -0
  29. entirius_django_contact_forms-3.0.0/src/django_contact_forms/admin/lead_creation_rule.py +15 -0
  30. entirius_django_contact_forms-3.0.0/src/django_contact_forms/admin/offline_conversion.py +51 -0
  31. entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/__init__.py +0 -0
  32. entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/admin/__init__.py +0 -0
  33. entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/admin/pagination.py +12 -0
  34. entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/admin/permissions.py +14 -0
  35. entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/admin/urls.py +95 -0
  36. entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/admin/views/__init__.py +0 -0
  37. entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/admin/views/attachment_views.py +72 -0
  38. entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/admin/views/booking_views.py +145 -0
  39. entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/admin/views/channel_integrations_views.py +49 -0
  40. entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/admin/views/form_type_views.py +61 -0
  41. entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/admin/views/lead_views.py +182 -0
  42. entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/admin/views/notification_config_views.py +204 -0
  43. entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/admin/views/offline_conversion_views.py +138 -0
  44. entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/admin/views/submission_views.py +194 -0
  45. entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/public/__init__.py +0 -0
  46. entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/public/authentication.py +50 -0
  47. entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/public/booking_views.py +185 -0
  48. entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/public/form_type_views.py +48 -0
  49. entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/public/throttling.py +56 -0
  50. entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/public/urls.py +33 -0
  51. entirius_django_contact_forms-3.0.0/src/django_contact_forms/api/public/views.py +206 -0
  52. entirius_django_contact_forms-3.0.0/src/django_contact_forms/apps.py +18 -0
  53. entirius_django_contact_forms-3.0.0/src/django_contact_forms/fixtures/django_contact_forms.cfg.yaml +187 -0
  54. entirius_django_contact_forms-3.0.0/src/django_contact_forms/management/__init__.py +0 -0
  55. entirius_django_contact_forms-3.0.0/src/django_contact_forms/management/commands/__init__.py +0 -0
  56. entirius_django_contact_forms-3.0.0/src/django_contact_forms/management/commands/forms-generate-api-key.py +39 -0
  57. entirius_django_contact_forms-3.0.0/src/django_contact_forms/migrations/0001_squashed_0014_alter_bookingtimewindow_id.py +681 -0
  58. entirius_django_contact_forms-3.0.0/src/django_contact_forms/migrations/__init__.py +0 -0
  59. entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/__init__.py +39 -0
  60. entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/api_key.py +42 -0
  61. entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/base_model.py +17 -0
  62. entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/booking.py +38 -0
  63. entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/booking_config.py +145 -0
  64. entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/booking_time_window.py +38 -0
  65. entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/channel.py +33 -0
  66. entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/contact_form.py +72 -0
  67. entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/contact_form_attachment.py +59 -0
  68. entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/contact_forms_settings.py +62 -0
  69. entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/form_notification_config.py +49 -0
  70. entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/form_type.py +52 -0
  71. entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/google_ads_config.py +48 -0
  72. entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/lead.py +88 -0
  73. entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/lead_creation_rule.py +59 -0
  74. entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/managers/__init__.py +0 -0
  75. entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/managers/contact_form_attachment.py +13 -0
  76. entirius_django_contact_forms-3.0.0/src/django_contact_forms/models/offline_conversion.py +56 -0
  77. entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/__init__.py +0 -0
  78. entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/requests/__init__.py +0 -0
  79. entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/requests/booking.py +38 -0
  80. entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/requests/lead.py +22 -0
  81. entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/requests/notification_config.py +48 -0
  82. entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/requests/status.py +13 -0
  83. entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/requests/submission.py +32 -0
  84. entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/responses/__init__.py +0 -0
  85. entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/responses/attachment.py +15 -0
  86. entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/responses/booking.py +76 -0
  87. entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/responses/channel_integrations.py +19 -0
  88. entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/responses/form_type.py +17 -0
  89. entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/responses/lead.py +50 -0
  90. entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/responses/notification_config.py +29 -0
  91. entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/responses/offline_conversion.py +32 -0
  92. entirius_django_contact_forms-3.0.0/src/django_contact_forms/schemas/responses/submission.py +36 -0
  93. entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/__init__.py +0 -0
  94. entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/booking_admin_service.py +76 -0
  95. entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/booking_notification_service.py +89 -0
  96. entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/booking_service.py +377 -0
  97. entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/calendar/__init__.py +0 -0
  98. entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/calendar/backends/__init__.py +0 -0
  99. entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/calendar/backends/base.py +44 -0
  100. entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/calendar/backends/google_calendar.py +56 -0
  101. entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/calendar/backends/local_only.py +68 -0
  102. entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/calendar/factory.py +53 -0
  103. entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/calendar/fomo.py +411 -0
  104. entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/channel_integrations_service.py +33 -0
  105. entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/contact_form_service.py +143 -0
  106. entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/conversions/__init__.py +3 -0
  107. entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/conversions/google_ads_uploader.py +127 -0
  108. entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/conversions/queue_service.py +129 -0
  109. entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/exceptions.py +25 -0
  110. entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/lead_creation_rule_service.py +103 -0
  111. entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/lead_service.py +132 -0
  112. entirius_django_contact_forms-3.0.0/src/django_contact_forms/services/notification_service.py +196 -0
  113. entirius_django_contact_forms-3.0.0/src/django_contact_forms/settings.py +53 -0
  114. entirius_django_contact_forms-3.0.0/src/django_contact_forms/signals/__init__.py +3 -0
  115. entirius_django_contact_forms-3.0.0/src/django_contact_forms/signals/handlers.py +14 -0
  116. entirius_django_contact_forms-3.0.0/src/django_contact_forms/signals/lead_signals.py +15 -0
  117. entirius_django_contact_forms-3.0.0/src/django_contact_forms/tasks/__init__.py +0 -0
  118. entirius_django_contact_forms-3.0.0/src/django_contact_forms/tasks/send_contact_form_email.py +135 -0
  119. entirius_django_contact_forms-3.0.0/src/django_contact_forms/tasks/upload_pending_conversions.py +34 -0
  120. entirius_django_contact_forms-3.0.0/src/django_contact_forms/templates/base/footer.html +1 -0
  121. entirius_django_contact_forms-3.0.0/src/django_contact_forms/templates/base/header.html +5 -0
  122. entirius_django_contact_forms-3.0.0/src/django_contact_forms/urls.py +23 -0
  123. entirius_django_contact_forms-3.0.0/src/django_contact_forms/utils/__init__.py +0 -0
  124. entirius_django_contact_forms-3.0.0/src/django_contact_forms/utils/decorators.py +37 -0
  125. entirius_django_contact_forms-3.0.0/src/django_contact_forms/utils/encrypted_field.py +76 -0
  126. entirius_django_contact_forms-3.0.0/src/django_contact_forms/utils/encryption.py +47 -0
  127. entirius_django_contact_forms-3.0.0/src/django_contact_forms/utils/exceptions.py +15 -0
  128. entirius_django_contact_forms-3.0.0/src/django_contact_forms/utils/payloads.py +41 -0
  129. entirius_django_contact_forms-3.0.0/src/django_contact_forms/utils/v2_errors.py +49 -0
  130. entirius_django_contact_forms-3.0.0/src/django_contact_forms/utils/workers.py +32 -0
  131. entirius_django_contact_forms-3.0.0/src/django_contact_forms/views/__init__.py +0 -0
  132. entirius_django_contact_forms-3.0.0/src/django_contact_forms/views/views.py +122 -0
  133. entirius_django_contact_forms-3.0.0/tests/__init__.py +0 -0
  134. entirius_django_contact_forms-3.0.0/tests/conftest.py +15 -0
  135. entirius_django_contact_forms-3.0.0/tests/factories.py +193 -0
  136. entirius_django_contact_forms-3.0.0/tests/settings.py +92 -0
  137. entirius_django_contact_forms-3.0.0/tests/test_admin_api.py +423 -0
  138. entirius_django_contact_forms-3.0.0/tests/test_booking_time_window.py +75 -0
  139. entirius_django_contact_forms-3.0.0/tests/test_calendar_backends.py +217 -0
  140. entirius_django_contact_forms-3.0.0/tests/test_fomo.py +730 -0
  141. entirius_django_contact_forms-3.0.0/tests/test_form_types.py +112 -0
  142. entirius_django_contact_forms-3.0.0/tests/test_multilang_booking.py +226 -0
  143. entirius_django_contact_forms-3.0.0/tests/test_public_api.py +341 -0
  144. entirius_django_contact_forms-3.0.0/tests/test_service_booking.py +290 -0
  145. entirius_django_contact_forms-3.0.0/tests/test_service_conversions.py +153 -0
  146. entirius_django_contact_forms-3.0.0/tests/test_service_lead.py +118 -0
  147. entirius_django_contact_forms-3.0.0/tests/test_services.py +277 -0
  148. entirius_django_contact_forms-3.0.0/tests/test_v2_api_bookings.py +345 -0
  149. entirius_django_contact_forms-3.0.0/tests/test_v2_api_bookings_admin.py +169 -0
  150. entirius_django_contact_forms-3.0.0/tests/test_v2_api_channel_integrations.py +114 -0
  151. entirius_django_contact_forms-3.0.0/tests/test_v2_api_leads.py +151 -0
  152. entirius_django_contact_forms-3.0.0/tests/test_v2_api_offline_conversions.py +74 -0
  153. entirius_django_contact_forms-3.0.0/uv.lock +1896 -0
@@ -0,0 +1,2 @@
1
+ # uv.lock is generated — collapse it in diffs and exclude from language stats.
2
+ uv.lock linguist-generated=true
@@ -0,0 +1,3 @@
1
+ # Stewardship (org owners) — auto-requested as PR reviewers.
2
+ # Code contributors are a different set: see maintainers in pyproject.toml.
3
+ * @zolvio @klo-chi
@@ -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,3 @@
1
+ This Source Code Form is subject to the terms of the Mozilla Public
2
+ License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
@@ -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