entirius-django-contentdb 5.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 (160) hide show
  1. entirius_django_contentdb-5.0.0/.gitattributes +2 -0
  2. entirius_django_contentdb-5.0.0/.github/workflows/ci.yml +49 -0
  3. entirius_django_contentdb-5.0.0/.github/workflows/publish.yml +18 -0
  4. entirius_django_contentdb-5.0.0/.gitignore +33 -0
  5. entirius_django_contentdb-5.0.0/.license-header.txt +3 -0
  6. entirius_django_contentdb-5.0.0/.pre-commit-config.yaml +39 -0
  7. entirius_django_contentdb-5.0.0/AGENTS.md +295 -0
  8. entirius_django_contentdb-5.0.0/CLAUDE.md +1 -0
  9. entirius_django_contentdb-5.0.0/LICENSE +373 -0
  10. entirius_django_contentdb-5.0.0/Makefile +16 -0
  11. entirius_django_contentdb-5.0.0/PKG-INFO +74 -0
  12. entirius_django_contentdb-5.0.0/README.md +38 -0
  13. entirius_django_contentdb-5.0.0/docs/api-reference.md +1101 -0
  14. entirius_django_contentdb-5.0.0/docs/erd-config.yaml +47 -0
  15. entirius_django_contentdb-5.0.0/docs/models-reference.md +766 -0
  16. entirius_django_contentdb-5.0.0/pyproject.toml +78 -0
  17. entirius_django_contentdb-5.0.0/src/django_contentdb/__init__.py +0 -0
  18. entirius_django_contentdb-5.0.0/src/django_contentdb/admin.py +324 -0
  19. entirius_django_contentdb-5.0.0/src/django_contentdb/api/__init__.py +0 -0
  20. entirius_django_contentdb-5.0.0/src/django_contentdb/api/v2/__init__.py +0 -0
  21. entirius_django_contentdb-5.0.0/src/django_contentdb/api/v2/pagination.py +11 -0
  22. entirius_django_contentdb-5.0.0/src/django_contentdb/api/v2/urls.py +18 -0
  23. entirius_django_contentdb-5.0.0/src/django_contentdb/api/v2/views/__init__.py +0 -0
  24. entirius_django_contentdb-5.0.0/src/django_contentdb/api/v2/views/author_views.py +128 -0
  25. entirius_django_contentdb-5.0.0/src/django_contentdb/api/v2/views/navigation_views.py +41 -0
  26. entirius_django_contentdb-5.0.0/src/django_contentdb/apps.py +15 -0
  27. entirius_django_contentdb-5.0.0/src/django_contentdb/enums.py +16 -0
  28. entirius_django_contentdb-5.0.0/src/django_contentdb/filters/__init__.py +7 -0
  29. entirius_django_contentdb-5.0.0/src/django_contentdb/filters/attribute.py +14 -0
  30. entirius_django_contentdb-5.0.0/src/django_contentdb/filters/content.py +118 -0
  31. entirius_django_contentdb-5.0.0/src/django_contentdb/filters/content_set.py +13 -0
  32. entirius_django_contentdb-5.0.0/src/django_contentdb/filters/draft.py +71 -0
  33. entirius_django_contentdb-5.0.0/src/django_contentdb/filters/image.py +21 -0
  34. entirius_django_contentdb-5.0.0/src/django_contentdb/filters/published.py +72 -0
  35. entirius_django_contentdb-5.0.0/src/django_contentdb/filters/route.py +13 -0
  36. entirius_django_contentdb-5.0.0/src/django_contentdb/image_manager.py +90 -0
  37. entirius_django_contentdb-5.0.0/src/django_contentdb/management/__init__.py +0 -0
  38. entirius_django_contentdb-5.0.0/src/django_contentdb/management/commands/__init__.py +0 -0
  39. entirius_django_contentdb-5.0.0/src/django_contentdb/management/commands/images-optimize.py +31 -0
  40. entirius_django_contentdb-5.0.0/src/django_contentdb/management/commands/propagate_content_channels.py +47 -0
  41. entirius_django_contentdb-5.0.0/src/django_contentdb/management/commands/sync_contentdb_channels.py +16 -0
  42. entirius_django_contentdb-5.0.0/src/django_contentdb/management/commands/sync_contentdb_languages.py +20 -0
  43. entirius_django_contentdb-5.0.0/src/django_contentdb/migrations/0001_squashed_0007_draft_is_system.py +637 -0
  44. entirius_django_contentdb-5.0.0/src/django_contentdb/migrations/__init__.py +0 -0
  45. entirius_django_contentdb-5.0.0/src/django_contentdb/models/__init__.py +26 -0
  46. entirius_django_contentdb-5.0.0/src/django_contentdb/models/access_rights.py +13 -0
  47. entirius_django_contentdb-5.0.0/src/django_contentdb/models/activity_log.py +23 -0
  48. entirius_django_contentdb-5.0.0/src/django_contentdb/models/attribute.py +79 -0
  49. entirius_django_contentdb-5.0.0/src/django_contentdb/models/attribute_set.py +25 -0
  50. entirius_django_contentdb-5.0.0/src/django_contentdb/models/attribute_to_set.py +11 -0
  51. entirius_django_contentdb-5.0.0/src/django_contentdb/models/attribute_value.py +55 -0
  52. entirius_django_contentdb-5.0.0/src/django_contentdb/models/author.py +49 -0
  53. entirius_django_contentdb-5.0.0/src/django_contentdb/models/category.py +39 -0
  54. entirius_django_contentdb-5.0.0/src/django_contentdb/models/content.py +47 -0
  55. entirius_django_contentdb-5.0.0/src/django_contentdb/models/content_attribute.py +11 -0
  56. entirius_django_contentdb-5.0.0/src/django_contentdb/models/content_channel.py +23 -0
  57. entirius_django_contentdb-5.0.0/src/django_contentdb/models/content_set.py +18 -0
  58. entirius_django_contentdb-5.0.0/src/django_contentdb/models/content_type.py +22 -0
  59. entirius_django_contentdb-5.0.0/src/django_contentdb/models/content_type_permission.py +35 -0
  60. entirius_django_contentdb-5.0.0/src/django_contentdb/models/deleted.py +11 -0
  61. entirius_django_contentdb-5.0.0/src/django_contentdb/models/draft.py +27 -0
  62. entirius_django_contentdb-5.0.0/src/django_contentdb/models/draft_author.py +19 -0
  63. entirius_django_contentdb-5.0.0/src/django_contentdb/models/draft_co_author.py +19 -0
  64. entirius_django_contentdb-5.0.0/src/django_contentdb/models/draft_to_content_set.py +14 -0
  65. entirius_django_contentdb-5.0.0/src/django_contentdb/models/image.py +33 -0
  66. entirius_django_contentdb-5.0.0/src/django_contentdb/models/image_tag.py +17 -0
  67. entirius_django_contentdb-5.0.0/src/django_contentdb/models/image_to_tag.py +17 -0
  68. entirius_django_contentdb-5.0.0/src/django_contentdb/models/language.py +22 -0
  69. entirius_django_contentdb-5.0.0/src/django_contentdb/models/published.py +18 -0
  70. entirius_django_contentdb-5.0.0/src/django_contentdb/models/route.py +43 -0
  71. entirius_django_contentdb-5.0.0/src/django_contentdb/models/thumbnail.py +37 -0
  72. entirius_django_contentdb-5.0.0/src/django_contentdb/permissions.py +39 -0
  73. entirius_django_contentdb-5.0.0/src/django_contentdb/schemas/__init__.py +0 -0
  74. entirius_django_contentdb-5.0.0/src/django_contentdb/schemas/navigation.py +69 -0
  75. entirius_django_contentdb-5.0.0/src/django_contentdb/schemas/requests/__init__.py +0 -0
  76. entirius_django_contentdb-5.0.0/src/django_contentdb/schemas/requests/author.py +101 -0
  77. entirius_django_contentdb-5.0.0/src/django_contentdb/schemas/responses/__init__.py +0 -0
  78. entirius_django_contentdb-5.0.0/src/django_contentdb/schemas/responses/author.py +48 -0
  79. entirius_django_contentdb-5.0.0/src/django_contentdb/schemas/responses/navigation.py +15 -0
  80. entirius_django_contentdb-5.0.0/src/django_contentdb/serializers/__init__.py +15 -0
  81. entirius_django_contentdb-5.0.0/src/django_contentdb/serializers/attribute.py +45 -0
  82. entirius_django_contentdb-5.0.0/src/django_contentdb/serializers/attribute_set.py +15 -0
  83. entirius_django_contentdb-5.0.0/src/django_contentdb/serializers/attribute_value.py +45 -0
  84. entirius_django_contentdb-5.0.0/src/django_contentdb/serializers/author.py +45 -0
  85. entirius_django_contentdb-5.0.0/src/django_contentdb/serializers/category.py +63 -0
  86. entirius_django_contentdb-5.0.0/src/django_contentdb/serializers/content.py +110 -0
  87. entirius_django_contentdb-5.0.0/src/django_contentdb/serializers/content_channel.py +18 -0
  88. entirius_django_contentdb-5.0.0/src/django_contentdb/serializers/content_set.py +49 -0
  89. entirius_django_contentdb-5.0.0/src/django_contentdb/serializers/content_type.py +33 -0
  90. entirius_django_contentdb-5.0.0/src/django_contentdb/serializers/draft.py +192 -0
  91. entirius_django_contentdb-5.0.0/src/django_contentdb/serializers/fields/__init__.py +8 -0
  92. entirius_django_contentdb-5.0.0/src/django_contentdb/serializers/fields/author_brief.py +45 -0
  93. entirius_django_contentdb-5.0.0/src/django_contentdb/serializers/fields/category.py +27 -0
  94. entirius_django_contentdb-5.0.0/src/django_contentdb/serializers/fields/content_set.py +14 -0
  95. entirius_django_contentdb-5.0.0/src/django_contentdb/serializers/fields/createable_slug_related.py +18 -0
  96. entirius_django_contentdb-5.0.0/src/django_contentdb/serializers/fields/draft_content_set_members.py +28 -0
  97. entirius_django_contentdb-5.0.0/src/django_contentdb/serializers/fields/extended_base64_image.py +16 -0
  98. entirius_django_contentdb-5.0.0/src/django_contentdb/serializers/fields/language.py +16 -0
  99. entirius_django_contentdb-5.0.0/src/django_contentdb/serializers/fields/published_content_set_members.py +46 -0
  100. entirius_django_contentdb-5.0.0/src/django_contentdb/serializers/image.py +63 -0
  101. entirius_django_contentdb-5.0.0/src/django_contentdb/serializers/image_tag.py +16 -0
  102. entirius_django_contentdb-5.0.0/src/django_contentdb/serializers/language.py +13 -0
  103. entirius_django_contentdb-5.0.0/src/django_contentdb/serializers/published.py +72 -0
  104. entirius_django_contentdb-5.0.0/src/django_contentdb/serializers/route.py +15 -0
  105. entirius_django_contentdb-5.0.0/src/django_contentdb/services/__init__.py +0 -0
  106. entirius_django_contentdb-5.0.0/src/django_contentdb/services/author_service.py +217 -0
  107. entirius_django_contentdb-5.0.0/src/django_contentdb/services/navigation_service.py +118 -0
  108. entirius_django_contentdb-5.0.0/src/django_contentdb/services/sync_service.py +95 -0
  109. entirius_django_contentdb-5.0.0/src/django_contentdb/settings.py +12 -0
  110. entirius_django_contentdb-5.0.0/src/django_contentdb/signals.py +16 -0
  111. entirius_django_contentdb-5.0.0/src/django_contentdb/tasks.py +62 -0
  112. entirius_django_contentdb-5.0.0/src/django_contentdb/tests.py +135 -0
  113. entirius_django_contentdb-5.0.0/src/django_contentdb/urls.py +68 -0
  114. entirius_django_contentdb-5.0.0/src/django_contentdb/utils.py +384 -0
  115. entirius_django_contentdb-5.0.0/src/django_contentdb/views/__init__.py +0 -0
  116. entirius_django_contentdb-5.0.0/src/django_contentdb/views/admin/__init__.py +15 -0
  117. entirius_django_contentdb-5.0.0/src/django_contentdb/views/admin/attribute.py +29 -0
  118. entirius_django_contentdb-5.0.0/src/django_contentdb/views/admin/attribute_set.py +25 -0
  119. entirius_django_contentdb-5.0.0/src/django_contentdb/views/admin/attribute_value.py +46 -0
  120. entirius_django_contentdb-5.0.0/src/django_contentdb/views/admin/categories.py +37 -0
  121. entirius_django_contentdb-5.0.0/src/django_contentdb/views/admin/content_channel.py +21 -0
  122. entirius_django_contentdb-5.0.0/src/django_contentdb/views/admin/content_set.py +35 -0
  123. entirius_django_contentdb-5.0.0/src/django_contentdb/views/admin/content_type.py +29 -0
  124. entirius_django_contentdb-5.0.0/src/django_contentdb/views/admin/draft.py +222 -0
  125. entirius_django_contentdb-5.0.0/src/django_contentdb/views/admin/image.py +29 -0
  126. entirius_django_contentdb-5.0.0/src/django_contentdb/views/admin/image_tag.py +21 -0
  127. entirius_django_contentdb-5.0.0/src/django_contentdb/views/admin/language.py +27 -0
  128. entirius_django_contentdb-5.0.0/src/django_contentdb/views/admin/published.py +94 -0
  129. entirius_django_contentdb-5.0.0/src/django_contentdb/views/admin/route.py +23 -0
  130. entirius_django_contentdb-5.0.0/src/django_contentdb/views/admin/view_content_permissions.py +41 -0
  131. entirius_django_contentdb-5.0.0/src/django_contentdb/views/admin/view_layout_permissions.py +41 -0
  132. entirius_django_contentdb-5.0.0/src/django_contentdb/views/public/__init__.py +14 -0
  133. entirius_django_contentdb-5.0.0/src/django_contentdb/views/public/attribute.py +27 -0
  134. entirius_django_contentdb-5.0.0/src/django_contentdb/views/public/attribute_set.py +23 -0
  135. entirius_django_contentdb-5.0.0/src/django_contentdb/views/public/attribute_value.py +44 -0
  136. entirius_django_contentdb-5.0.0/src/django_contentdb/views/public/author.py +41 -0
  137. entirius_django_contentdb-5.0.0/src/django_contentdb/views/public/categories.py +54 -0
  138. entirius_django_contentdb-5.0.0/src/django_contentdb/views/public/content_channel.py +19 -0
  139. entirius_django_contentdb-5.0.0/src/django_contentdb/views/public/content_set.py +33 -0
  140. entirius_django_contentdb-5.0.0/src/django_contentdb/views/public/content_type.py +27 -0
  141. entirius_django_contentdb-5.0.0/src/django_contentdb/views/public/draft.py +72 -0
  142. entirius_django_contentdb-5.0.0/src/django_contentdb/views/public/image.py +27 -0
  143. entirius_django_contentdb-5.0.0/src/django_contentdb/views/public/image_tag.py +19 -0
  144. entirius_django_contentdb-5.0.0/src/django_contentdb/views/public/language.py +25 -0
  145. entirius_django_contentdb-5.0.0/src/django_contentdb/views/public/published.py +121 -0
  146. entirius_django_contentdb-5.0.0/src/django_contentdb/views/public/route.py +21 -0
  147. entirius_django_contentdb-5.0.0/src/django_contentdb/viewsets.py +185 -0
  148. entirius_django_contentdb-5.0.0/tests/__init__.py +0 -0
  149. entirius_django_contentdb-5.0.0/tests/conftest.py +30 -0
  150. entirius_django_contentdb-5.0.0/tests/settings.py +71 -0
  151. entirius_django_contentdb-5.0.0/tests/test_author_api.py +494 -0
  152. entirius_django_contentdb-5.0.0/tests/test_author_model.py +228 -0
  153. entirius_django_contentdb-5.0.0/tests/test_author_service.py +371 -0
  154. entirius_django_contentdb-5.0.0/tests/test_channel_filters.py +83 -0
  155. entirius_django_contentdb-5.0.0/tests/test_content_channel.py +69 -0
  156. entirius_django_contentdb-5.0.0/tests/test_draft_channels.py +50 -0
  157. entirius_django_contentdb-5.0.0/tests/test_navigation_api.py +528 -0
  158. entirius_django_contentdb-5.0.0/tests/test_published_siblings.py +437 -0
  159. entirius_django_contentdb-5.0.0/tests/test_sync_service.py +68 -0
  160. entirius_django_contentdb-5.0.0/uv.lock +2045 -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,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,295 @@
1
+ # AGENTS.md
2
+
3
+ ## Quick Reference
4
+
5
+ Content management module for the Volkanos ecommerce platform. JSON-based page content with draft/publish workflow, routing, access control, media management, and blog authoring. v1 API for content CRUD, v2 API for author management (Pydantic + drf-spectacular).
6
+
7
+ **Tech:** Python >=3.10, Django >=4.0, DRF, django-filter, Pillow, Pydantic, drf-spectacular, djangorestframework-simplejwt
8
+
9
+ ## Commands
10
+
11
+ | Command | Meaning |
12
+ |---|---|
13
+ | `make install` | sync dependencies (uv, incl. extras) |
14
+ | `make check` | lint + format-check (ruff) |
15
+ | `make fix` | auto-fix lint + format |
16
+ | `make test` | test suite (pytest + pytest-django) |
17
+
18
+ ## Conventions
19
+
20
+ - English only: code, docs, commits, branches, PRs.
21
+ - MPL-2.0: every non-trivial source file carries the license header (pre-commit inserts it).
22
+ - Toolchain: uv + ruff + hatchling + pytest; all config in `pyproject.toml`; `uv.lock` committed.
23
+ - Git flow: `master` (production) + `develop` (integration); changes land via PR; semver tag on `master`.
24
+ - Never rename the package / Django app_label / DB table prefix `django_contentdb` — it is a schema contract.
25
+ - Migrations are part of the public contract — never edit an already released migration.
26
+ - Default: do not commit — git is the user's call.
27
+
28
+ ## Architecture
29
+
30
+ ```
31
+ src/django_contentdb/
32
+ ├── models/ # 26 ORM models (one file per entity)
33
+ │ ├── content.py # Content (UUID pk, JSONField body)
34
+ │ ├── content_type.py # ContentType (slug↑, is_layout_extender, supports_authors)
35
+ │ ├── content_channel.py # ContentChannel (idx↑, name, default_language FK, is_default)
36
+ │ ├── draft.py # Draft (FK→ContentType, O2O→Content, FK→Language, M2M→ContentChannel, M2M→Author)
37
+ │ ├── published.py # Published (FK→Draft, O2O→Content, immutable snapshot)
38
+ │ ├── route.py # Route (url↑, placement, M2M→Draft)
39
+ │ ├── author.py # Author (UUID, name, slug↑, role/bio/tag _t9n, photo FK→Image)
40
+ │ ├── draft_author.py # DraftAuthor (through: Draft↔Author, position)
41
+ │ ├── draft_co_author.py # DraftCoAuthor (through: Draft↔Author, position)
42
+ │ ├── attribute.py # Attribute (slug↑, 6 type flags, filter/search flags)
43
+ │ ├── attribute_set.py # AttributeSet (slug↑, M2M→Attribute)
44
+ │ ├── attribute_value.py # AttributeValue (typed value fields)
45
+ │ ├── attribute_to_set.py # AttributeToSet (M2M through)
46
+ │ ├── content_attribute.py # ContentAttribute (M2M through: Content↔AttributeValue)
47
+ │ ├── image.py # Image (UUID, HashedImageField, SHA256 dedup)
48
+ │ ├── thumbnail.py # Thumbnail (method, source→Image, unique per dims)
49
+ │ ├── image_tag.py # ImageTag (slug↑)
50
+ │ ├── image_to_tag.py # ImageToTag (M2M through)
51
+ │ ├── access_rights.py # AccessRights (access_level = PK, pk=1 = public) [DEPRECATED]
52
+ │ ├── language.py # Language (iso2↑, iso3↑, name_en, name_pl)
53
+ │ ├── category.py # Category (UUID, auto url_key from name)
54
+ │ ├── content_type_permission.py # ContentTypePermission (action×content_type unique)
55
+ │ ├── content_set.py # ContentSet (UUID, M2M→Draft)
56
+ │ ├── draft_to_content_set.py # DraftToContentSet (M2M through)
57
+ │ ├── activity_log.py # ActivityLog (GenericFK audit trail)
58
+ │ └── deleted.py # Deleted (O2O→Content, soft-delete audit)
59
+ │
60
+ ├── services/ # Business logic
61
+ │ ├── sync_service.py # sync_channels_from_pim(), sync_languages_from_pim()
62
+ │ └── author_service.py # CRUD + reassignment + set_draft_authors
63
+ │
64
+ ├── api/ # v2 API (Pydantic + drf-spectacular)
65
+ │ └── v2/
66
+ │ ├── views/author_views.py # AuthorViewSet (JWT + IsAdminUser)
67
+ │ ├── urls.py # /api/contentdb/v2/admin/authors/
68
+ │ └── pagination.py # AdminPageNumberPagination (20/page, max 100)
69
+ │
70
+ ├── schemas/ # Pydantic schemas (v2 API)
71
+ │ ├── requests/author.py # CreateAuthorRequest, UpdateAuthorRequest, DeleteAuthorRequest
72
+ │ └── responses/author.py # AuthorResponse, AuthorBriefResponse, AuthorListResponse
73
+ │
74
+ ├── views/ # DRF ViewSets (v1 admin + public mirrors)
75
+ │ ├── admin/ # 17 ViewSets + 2 function views (CRUD + publish)
76
+ │ └── public/ # 16 ViewSets (read-only)
77
+ │
78
+ ├── serializers/ # DRF serializers (v1 API)
79
+ │ ├── content.py # ContentSerializer, DraftContentSerializer
80
+ │ ├── content_channel.py # ContentChannelSerializer
81
+ │ ├── published.py # PublishedContentSerializer
82
+ │ ├── attribute.py, attribute_value.py, attribute_set.py
83
+ │ ├── content_type.py, route.py, language.py, category.py
84
+ │ ├── image.py, image_tag.py, content_set.py
85
+ │ └── fields/ # Custom fields (AuthorBrief, CreateableSlugRelated, etc.)
86
+ │
87
+ ├── filters/ # django-filter classes
88
+ │ ├── published.py # access_rights, channel, language, routes, author
89
+ │ ├── draft.py # access_rights, channel, language, routes, author
90
+ │ ├── content_set.py, image.py
91
+ │ └── ordering.py # sort by created_at
92
+ │
93
+ ├── viewsets.py # Base classes: ContentDBModelViewSet, ROContentDBModelViewSet
94
+ ├── permissions.py # ContentTypePermission (action-based per content_type)
95
+ ├── enums.py # Action: CREATE, UPDATE, DELETE, PUBLISH, VIEW
96
+ ├── settings.py # ADMIN_BASE_URL, PUBLIC_BASE_URL, THUMBNAIL_QUALITY, IMAGE_MAX_WIDTH
97
+ ├── utils.py # StandardPagination, HashedImageField, DjangoAuth, exception_handler
98
+ ├── image_manager.py # PIL utilities: resize, crop, transparency removal
99
+ ├── management/commands/
100
+ │ ├── sync_contentdb_channels.py # Sync ContentChannels from PIM
101
+ │ ├── sync_contentdb_languages.py # Sync Languages from django_regional
102
+ │ └── propagate_content_channels.py # Assign channels to drafts with empty M2M
103
+ ├── admin.py # Django admin (20 registered models, Author + DraftAuthor/DraftCoAuthor inlines)
104
+ ├── urls.py # Admin + public routers (19 routes each)
105
+ └── tasks.py # Celery: image optimization
106
+ ```
107
+
108
+ Layer rule: v1 views use DRF serializers directly (no service layer for CRUD). v2 Author API uses Pydantic schemas + service layer. `viewsets.py` base classes auto-log to ActivityLog. Sync logic lives in `services/sync_service.py`, author CRUD in `services/author_service.py`.
109
+
110
+ ---
111
+
112
+ ## Content Pipeline
113
+
114
+ ```
115
+ Content (UUID pk, JSON blob: {tiles, sections, tiles_order, sections_order})
116
+ → Draft (links ContentType + Content + Language + ContentChannel M2M)
117
+ → Published (immutable snapshot of Draft + Content)
118
+ → Route (URL path, M2M→Draft, placement: top|bottom)
119
+ ```
120
+
121
+ - **ContentType** defines the template: `static-page`, `header`, `blog-post`, `product-rich-content`, `category-rich-content`
122
+ - **ContentType.is_layout_extender** = true → header type (separate API routes)
123
+ - **ContentType.supports_authors** = true → enables author picker in CMS (only `blog-post` by default)
124
+ - **Author** — author profiles with name, slug, role/bio/tag translations, photo, contacts, social links
125
+ - **DraftAuthor / DraftCoAuthor** — through-tables linking Draft ↔ Author with position ordering
126
+ - **ContentChannel** — local mirror of PIM Channel (idx↑, name, default_language FK, is_default). Syncs from PIM via admin action or management command. Empty `Draft.channels` M2M = content is public (all channels).
127
+ - **AccessRights** — DEPRECATED. Replaced by ContentChannel. Kept for backward compatibility during transition.
128
+ - Content/Route `delete()` prevents deletion of `home` and `header` records
129
+
130
+ ---
131
+
132
+ ## API
133
+
134
+ **Response wrapper** (all endpoints): `{"meta": {"status": "OK|CREATED|UPDATED|DELETED|ERROR"}, "data": ...}`
135
+
136
+ **Pagination:** `StandardPagination` — page_size=6, max=100, query param: `limit`
137
+
138
+ ### Admin API — `/{ADMIN_BASE_URL}/contentdb/{version}/`
139
+
140
+ Default: `/api-admin/contentdb/v1/`. Auth: DjangoAuth + IsAdminUser | ContentTypePermission.
141
+
142
+ ```
143
+ contentdb/v1/
144
+ ├── attributes/ CRUD
145
+ │ └── {attribute_slug}/values/ CRUD (nested)
146
+ ├── attribute-sets/ CRUD
147
+ ├── content-types/ CRUD
148
+ ├── routes/ CRUD
149
+ ├── content-sets/ CRUD
150
+ ├── content/{content_type}/ CRUD drafts
151
+ │ └── {uid}/published/ POST (publish), DELETE (unpublish)
152
+ ├── published/{content_type}/ RO
153
+ ├── layout-extender-types/ CRUD (is_layout_extender=True only)
154
+ ├── layout-extender-sets/ CRUD
155
+ ├── layout-extender/{content_type}/ CRUD layout drafts
156
+ ├── layout-extender-published/... RO
157
+ ├── images/ CRUD
158
+ ├── image-tags/ CRUD
159
+ ├── languages/ CRUD
160
+ ├── channels/ RO (ContentChannel list/retrieve)
161
+ ├── category/ CRUD
162
+ ├── content-permissions/ FBV (manage per-type permissions)
163
+ └── layout-extender-permissions/ FBV
164
+ ```
165
+
166
+ ### Public API — `/{PUBLIC_BASE_URL}/contentdb/{version}/`
167
+
168
+ Default: `/api/contentdb/v1/`. Auth: AllowAny.
169
+
170
+ Same routes as admin but **read-only** (list + retrieve only). Key query:
171
+
172
+ ```
173
+ GET /api/contentdb/v1/published/{content_type_slug}/?routes=home&language=EN&channel=default
174
+ ```
175
+
176
+ Filters: `routes` (url), `language` (iso2), `channel` (ContentChannel idx), `author` (slug or UUID), `access_rights` (deprecated). Sort: `created_at` (maps to draft's created_at). Channel filter: returns content assigned to that channel + public content (empty channels M2M). Author filter: searches both authors and co-authors on drafts.
177
+
178
+ Full endpoint details: `docs/api-reference.md`
179
+
180
+ ### v2 Admin API — /api/contentdb/v2/admin/
181
+
182
+ | Method | Endpoint | Purpose |
183
+ |--------|----------|---------|
184
+ | GET | /authors/ | List authors (search, is_active, channel post count) |
185
+ | POST | /authors/ | Create author (auto-slug) |
186
+ | GET | /authors/{uid}/ | Author detail |
187
+ | PATCH | /authors/{uid}/ | Update author |
188
+ | DELETE | /authors/{uid}/ | Delete with reassignment |
189
+
190
+ Auth: JWT + IsAdminUser. Pagination: 20/page, max 100. Pydantic schemas for request/response validation.
191
+
192
+ ---
193
+
194
+ ## Data Model — Authoring
195
+
196
+ | Entity | Key Fields |
197
+ |--------|------------|
198
+ | Author | uid (UUID), slug↑, name, role_t9n, description_t9n, tag_t9n, photo→Image, contacts, social_profiles, is_active |
199
+ | DraftAuthor | draft→Draft, author→Author, position (unique together: draft+author) |
200
+ | DraftCoAuthor | draft→Draft, author→Author, position (unique together: draft+author) |
201
+
202
+ ---
203
+
204
+ ## Image Handling
205
+
206
+ - **HashedImageField** — SHA256 content-addressed storage: `aa/bb/cccccccc...{ext}`
207
+ - **Auto-resize** on upload: max width = `CONTENTDB_IMAGE_MAX_WIDTH` (default 2560)
208
+ - **Thumbnail** model: unique per (method, source, width, height)
209
+ - **Deduplication**: same file content → same hash → same path (no duplicates)
210
+ - Uses `image_transformations` library with PIL fallback
211
+
212
+ ---
213
+
214
+ ## Key Enums
215
+
216
+ | Enum | Values |
217
+ |------|--------|
218
+ | **Action** | CREATE, UPDATE, DELETE, PUBLISH, VIEW |
219
+ | **Thumbnail.method** | "optimize" |
220
+ | **Route.placement** | "top", "bottom" |
221
+
222
+ ---
223
+
224
+ ## Settings
225
+
226
+ | Setting | Default | Description |
227
+ |---------|---------|-------------|
228
+ | `API_ADMIN_BASE_URL` | `/api-admin/` | Admin API prefix |
229
+ | `API_PUBLIC_BASE_URL` | `/api/` | Public API prefix |
230
+ | `CONTENTDB_THUMBNAIL_QUALITY` | 60 | JPEG thumbnail quality |
231
+ | `CONTENTDB_IMAGE_MAX_WIDTH` | 2560 | Max upload width (0 = no resize) |
232
+
233
+ ---
234
+
235
+ ## Testing
236
+
237
+ Factory classes available:
238
+
239
+ ```
240
+ tests/__init__.py: AttributeSetFactory, ContentTypeFactory, ContentFactory, DraftFactory, PublishedFactory, ContentChannelFactory
241
+ ```
242
+
243
+ ```bash
244
+ make test # pytest (requires Postgres — set DATABASE_URL or use the CI default)
245
+ ```
246
+
247
+ ---
248
+
249
+ ## Management Commands
250
+
251
+ ```bash
252
+ # Sync commands (run inside Django service context)
253
+ python manage.py sync_contentdb_channels # Sync from PIM
254
+ python manage.py sync_contentdb_languages # Sync from django_regional
255
+ python manage.py propagate_content_channels --channel=default # Assign channel to public content
256
+ ```
257
+
258
+ ---
259
+
260
+ ## Gotchas
261
+
262
+ - Content/Route `delete()` protects `home` and `header` from full removal — raises `ValidationError` (HTTP 400 via `standard_exception_handler`). Duplicates (multiple drafts sharing the same `home` route across channels) are deletable; only the last instance is blocked. `bypass=True` skips validation. System content (`Draft.is_system=True`) is always blocked regardless of routes.
263
+ - `AccessRights` is DEPRECATED — replaced by `ContentChannel`. Both `access_rights` and `channel` query params work during transition
264
+ - `ContentChannel` syncs from PIM via `services/sync_service.py`. When PIM not installed, sync returns 0 gracefully
265
+ - Empty `Draft.channels` M2M = public content (visible to all channel filters)
266
+ - Published is immutable — creating a new Published from a Draft creates a new snapshot, doesn't update existing
267
+ - Admin API prefix is `/api-admin/` (not `/api/v2/`) — this is v1 architecture, pre-Pydantic
268
+ - DRF serializers (not Pydantic) — no drf-spectacular, no OpenAPI auto-generation
269
+ - `HashedImageField` uses SHA256 of content for path — same image uploaded twice = same file on disk
270
+ - `ContentTypePermission` checks `content_type` slug from URL kwargs — requires content_type in URL path
271
+ - `standard_exception_handler` wraps ALL errors in `{"meta": {...}, "data": ...}` format
272
+ - Layout extender routes are separate from content routes — `is_layout_extender=True` ContentTypes use `layout-extender/*` paths
273
+ - Author cannot appear in both `authors` and `co_authors` M2M on the same Draft — validated in service layer and API
274
+ - v2 Author endpoints use JWT + IsAdminUser (not DjangoAuth like v1)
275
+ - ContentType.supports_authors controls which types show author picker in CMS — only blog-post by default
276
+ - Published author data is a live reference through draft — changing authors on draft changes what published shows without re-publishing
277
+ - `?author=` filter on v1 endpoints searches both authors and co-authors by slug or UUID
278
+
279
+ ---
280
+
281
+ ## Module Boundaries
282
+
283
+ **Does:** Store/serve JSON page content, draft/publish lifecycle, image uploads + thumbnails, attribute metadata, access control, activity audit logging, blog author management
284
+
285
+ **Does NOT:** Render content (storefront's job), define page layouts (CMS Blueprint's job). Optional sync from PIM/django_regional (graceful fallback when not installed)
286
+
287
+ ---
288
+
289
+ ## Reference Docs
290
+
291
+ | File | Content |
292
+ |------|---------|
293
+ | `docs/api-reference.md` | Full API surface: all endpoints, serializer fields, filters, permissions |
294
+ | `docs/models-reference.md` | Complete model inventory: fields, relationships, constraints, enums |
295
+ | `docs/erd-config.yaml` | ERD diagram config for `make erd` |
@@ -0,0 +1 @@
1
+ @AGENTS.md