django-cfg 1.4.101__py3-none-any.whl → 1.4.103__py3-none-any.whl
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.
Potentially problematic release.
This version of django-cfg might be problematic. Click here for more details.
- django_cfg/__init__.py +1 -1
- django_cfg/models/django/crypto_fields.py +49 -6
- django_cfg/modules/django_client/management/commands/generate_client.py +24 -4
- django_cfg/modules/nextjs_admin/models/config.py +13 -0
- django_cfg/pyproject.toml +1 -1
- django_cfg/templates/admin/DUAL_TAB_ARCHITECTURE.md +504 -0
- django_cfg/templates/admin/index.html +0 -50
- django_cfg/templatetags/django_cfg.py +56 -27
- {django_cfg-1.4.101.dist-info → django_cfg-1.4.103.dist-info}/METADATA +1 -1
- {django_cfg-1.4.101.dist-info → django_cfg-1.4.103.dist-info}/RECORD +13 -12
- {django_cfg-1.4.101.dist-info → django_cfg-1.4.103.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.101.dist-info → django_cfg-1.4.103.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.101.dist-info → django_cfg-1.4.103.dist-info}/licenses/LICENSE +0 -0
django_cfg/__init__.py
CHANGED
|
@@ -61,7 +61,11 @@ class CryptoFieldsConfig(BaseModel):
|
|
|
61
61
|
# === Django Revision Settings (for django-audit-fields dependency) ===
|
|
62
62
|
ignore_git_dir: bool = Field(
|
|
63
63
|
default=True,
|
|
64
|
-
description=
|
|
64
|
+
description=(
|
|
65
|
+
"Ignore git directory for django-revision. "
|
|
66
|
+
"When True, version is discovered from: package metadata → pyproject.toml → VERSION file. "
|
|
67
|
+
"Prevents 'not recommended' warning from settings.REVISION fallback."
|
|
68
|
+
)
|
|
65
69
|
)
|
|
66
70
|
|
|
67
71
|
def to_django_settings(self, base_dir: Path, is_production: bool, debug: bool) -> dict:
|
|
@@ -108,13 +112,52 @@ class CryptoFieldsConfig(BaseModel):
|
|
|
108
112
|
}
|
|
109
113
|
|
|
110
114
|
# Disable django-revision git integration (required by django-audit-fields)
|
|
111
|
-
# RevisionField will use package metadata
|
|
115
|
+
# RevisionField will use package metadata, pyproject.toml, or VERSION file instead
|
|
112
116
|
if self.ignore_git_dir:
|
|
113
117
|
settings["DJANGO_REVISION_IGNORE_WORKING_DIR"] = True
|
|
114
|
-
#
|
|
115
|
-
|
|
116
|
-
#
|
|
117
|
-
|
|
118
|
+
# Allow django-revision to discover version from:
|
|
119
|
+
# 1. Package metadata (version())
|
|
120
|
+
# 2. pyproject.toml [project][version]
|
|
121
|
+
# 3. VERSION file (BASE_DIR/VERSION)
|
|
122
|
+
# Do NOT set REVISION here to avoid "not recommended" warning
|
|
123
|
+
|
|
124
|
+
# Create VERSION file if it doesn't exist (prevents fallback to settings.REVISION)
|
|
125
|
+
version_file = base_dir / "VERSION"
|
|
126
|
+
if not version_file.exists():
|
|
127
|
+
try:
|
|
128
|
+
# Try to get version from pyproject.toml or package metadata
|
|
129
|
+
version_to_write = "1.0.0" # Default fallback
|
|
130
|
+
|
|
131
|
+
# Try pyproject.toml first
|
|
132
|
+
pyproject_file = base_dir / "pyproject.toml"
|
|
133
|
+
if pyproject_file.exists():
|
|
134
|
+
try:
|
|
135
|
+
import tomllib
|
|
136
|
+
except ImportError:
|
|
137
|
+
# Python < 3.11
|
|
138
|
+
try:
|
|
139
|
+
import tomli as tomllib # type: ignore
|
|
140
|
+
except ImportError:
|
|
141
|
+
tomllib = None # type: ignore
|
|
142
|
+
|
|
143
|
+
if tomllib:
|
|
144
|
+
try:
|
|
145
|
+
with open(pyproject_file, "rb") as f:
|
|
146
|
+
pyproject_data = tomllib.load(f)
|
|
147
|
+
version_to_write = pyproject_data.get("project", {}).get("version", version_to_write)
|
|
148
|
+
except Exception:
|
|
149
|
+
pass # Use default version
|
|
150
|
+
|
|
151
|
+
# Write VERSION file
|
|
152
|
+
version_file.write_text(version_to_write + "\n")
|
|
153
|
+
|
|
154
|
+
import logging
|
|
155
|
+
logger = logging.getLogger(__name__)
|
|
156
|
+
logger.info(f"Created VERSION file at {version_file} with version {version_to_write}")
|
|
157
|
+
except Exception as e:
|
|
158
|
+
import logging
|
|
159
|
+
logger = logging.getLogger(__name__)
|
|
160
|
+
logger.warning(f"Could not create VERSION file: {e}")
|
|
118
161
|
|
|
119
162
|
return settings
|
|
120
163
|
|
|
@@ -673,7 +673,11 @@ class Command(BaseCommand):
|
|
|
673
673
|
))
|
|
674
674
|
|
|
675
675
|
# Create ZIP archive for Django static (Docker-ready)
|
|
676
|
-
|
|
676
|
+
# Use solution project's BASE_DIR from Django settings
|
|
677
|
+
from django.conf import settings as django_settings
|
|
678
|
+
solution_base_dir = django_settings.BASE_DIR
|
|
679
|
+
|
|
680
|
+
django_static_zip = nextjs_config.get_static_zip_path(solution_base_dir)
|
|
677
681
|
|
|
678
682
|
try:
|
|
679
683
|
# Ensure static directory exists
|
|
@@ -707,11 +711,14 @@ class Command(BaseCommand):
|
|
|
707
711
|
# Get ZIP size
|
|
708
712
|
zip_size_mb = django_static_zip.stat().st_size / (1024 * 1024)
|
|
709
713
|
|
|
714
|
+
# Show relative path from solution BASE_DIR
|
|
715
|
+
relative_zip_path = django_static_zip.relative_to(solution_base_dir)
|
|
716
|
+
|
|
710
717
|
self.stdout.write(self.style.SUCCESS(
|
|
711
|
-
f" ✅ Created ZIP archive: {
|
|
718
|
+
f" ✅ Created ZIP archive: {relative_zip_path} ({zip_size_mb:.1f}MB)"
|
|
712
719
|
))
|
|
713
720
|
self.stdout.write(self.style.SUCCESS(
|
|
714
|
-
f" 📍 ZIP location: {django_static_zip
|
|
721
|
+
f" 📍 ZIP location: {django_static_zip}"
|
|
715
722
|
))
|
|
716
723
|
self.stdout.write(self.style.SUCCESS(
|
|
717
724
|
" ℹ️ This ZIP is used by NextJsAdminView (Tab 2: External Admin)"
|
|
@@ -729,17 +736,30 @@ class Command(BaseCommand):
|
|
|
729
736
|
self.stdout.write(self.style.ERROR(
|
|
730
737
|
f"\n❌ Next.js build failed with exit code {result.returncode}"
|
|
731
738
|
))
|
|
739
|
+
|
|
740
|
+
# Show full error output
|
|
732
741
|
if result.stderr:
|
|
733
|
-
self.stdout.write(self.style.ERROR(f"
|
|
742
|
+
self.stdout.write(self.style.ERROR(f"\n stderr:\n{result.stderr}"))
|
|
743
|
+
if result.stdout:
|
|
744
|
+
self.stdout.write(self.style.ERROR(f"\n stdout:\n{result.stdout}"))
|
|
745
|
+
|
|
746
|
+
# Exit on build failure
|
|
747
|
+
raise CommandError(
|
|
748
|
+
f"Next.js build failed with exit code {result.returncode}. "
|
|
749
|
+
"Fix the build errors and try again."
|
|
750
|
+
)
|
|
734
751
|
|
|
735
752
|
except subprocess.TimeoutExpired:
|
|
736
753
|
self.stdout.write(self.style.ERROR(
|
|
737
754
|
"\n❌ Next.js build timed out (5 minutes)"
|
|
738
755
|
))
|
|
756
|
+
raise CommandError("Next.js build timed out after 5 minutes")
|
|
757
|
+
|
|
739
758
|
except Exception as build_error:
|
|
740
759
|
self.stdout.write(self.style.ERROR(
|
|
741
760
|
f"\n❌ Build command failed: {build_error}"
|
|
742
761
|
))
|
|
762
|
+
raise CommandError(f"Next.js build command failed: {build_error}")
|
|
743
763
|
|
|
744
764
|
except Exception as e:
|
|
745
765
|
self.stdout.write(self.style.ERROR(f"\n❌ Failed to build Next.js admin: {e}"))
|
|
@@ -165,6 +165,19 @@ class NextJsAdminConfig(BaseModel):
|
|
|
165
165
|
"""Get tab title with default."""
|
|
166
166
|
return self.tab_title or "Next.js Admin"
|
|
167
167
|
|
|
168
|
+
def get_static_zip_path(self, solution_base_dir):
|
|
169
|
+
"""
|
|
170
|
+
Get path to nextjs_admin.zip for Django static serving.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
solution_base_dir: Solution project BASE_DIR (from settings.BASE_DIR)
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Path: Path to nextjs_admin.zip (e.g., solution/projects/django/static/nextjs_admin.zip)
|
|
177
|
+
"""
|
|
178
|
+
from pathlib import Path
|
|
179
|
+
return Path(solution_base_dir) / "static" / "nextjs_admin.zip"
|
|
180
|
+
|
|
168
181
|
# =================================================================
|
|
169
182
|
# Validators
|
|
170
183
|
# =================================================================
|
django_cfg/pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "django-cfg"
|
|
7
|
-
version = "1.4.
|
|
7
|
+
version = "1.4.103"
|
|
8
8
|
description = "Modern Django framework with type-safe Pydantic v2 configuration, Next.js admin integration, real-time WebSockets, and 8 enterprise apps. Replace settings.py with validated models, 90% less code. Production-ready with AI agents, auto-generated TypeScript clients, and zero-config features."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
keywords = [ "django", "configuration", "pydantic", "settings", "type-safety", "pydantic-settings", "django-environ", "startup-validation", "ide-autocomplete", "nextjs-admin", "react-admin", "websocket", "centrifugo", "real-time", "typescript-generation", "ai-agents", "enterprise-django", "django-settings", "type-safe-config", "modern-django",]
|
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
# Dual-Tab Admin Architecture
|
|
2
|
+
|
|
3
|
+
This document explains the architecture and logic behind Django-CFG's dual-tab admin interface, which allows seamless integration of both built-in and external Next.js admin panels.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Django-CFG admin interface supports **two independent Next.js admin panels** running side-by-side in separate tabs:
|
|
8
|
+
|
|
9
|
+
1. **Tab 1: Built-in Dashboard** - The default Next.js admin panel shipped with Django-CFG
|
|
10
|
+
2. **Tab 2: External Next.js Admin** - A custom Next.js admin panel from your solution project
|
|
11
|
+
|
|
12
|
+
Each tab operates independently with its own:
|
|
13
|
+
- Development server port
|
|
14
|
+
- Static file serving path
|
|
15
|
+
- Fallback mechanism
|
|
16
|
+
- iframe communication channel
|
|
17
|
+
|
|
18
|
+
## Architecture Diagram
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
22
|
+
│ Django Admin Interface │
|
|
23
|
+
│ ┌───────────────────────────────┬───────────────────────────┐ │
|
|
24
|
+
│ │ Tab 1: Built-in Dashboard │ Tab 2: External Admin │ │
|
|
25
|
+
│ └───────────────────────────────┴───────────────────────────┘ │
|
|
26
|
+
│ │
|
|
27
|
+
│ Dev Mode: │ Dev Mode: │
|
|
28
|
+
│ ↓ http://localhost:3777/admin │ ↓ http://localhost:3000/admin│
|
|
29
|
+
│ │
|
|
30
|
+
│ Fallback (Static): │ Fallback (Static): │
|
|
31
|
+
│ ↓ /cfg/admin/admin/ │ ↓ /cfg/nextjs-admin/admin/ │
|
|
32
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Port Allocation
|
|
36
|
+
|
|
37
|
+
| Tab | Purpose | Dev Port | Static Path |
|
|
38
|
+
|-----|---------|----------|-------------|
|
|
39
|
+
| Tab 1 | Built-in Dashboard | `3777` | `/cfg/admin/admin/` |
|
|
40
|
+
| Tab 2 | External Next.js Admin | `3000` | `/cfg/nextjs-admin/admin/` |
|
|
41
|
+
|
|
42
|
+
### Why Different Ports?
|
|
43
|
+
|
|
44
|
+
Using separate ports prevents conflicts and allows both dev servers to run simultaneously:
|
|
45
|
+
- **Port 3777**: Reserved for Django-CFG's built-in admin panel
|
|
46
|
+
- **Port 3000**: Reserved for solution project's custom admin panel
|
|
47
|
+
|
|
48
|
+
## URL Resolution Logic
|
|
49
|
+
|
|
50
|
+
### Development Mode (DEBUG=True)
|
|
51
|
+
|
|
52
|
+
**Server-Side (Python)**: Always returns dev server URLs in DEBUG mode:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
def nextjs_admin_url(path=''):
|
|
56
|
+
"""Built-in Dashboard (Tab 1)"""
|
|
57
|
+
if settings.DEBUG:
|
|
58
|
+
return f'http://localhost:3777/admin/{path}'
|
|
59
|
+
else:
|
|
60
|
+
return f'/cfg/admin/admin/{path}'
|
|
61
|
+
|
|
62
|
+
def nextjs_external_admin_url(route=''):
|
|
63
|
+
"""External Next.js Admin (Tab 2)"""
|
|
64
|
+
if settings.DEBUG:
|
|
65
|
+
return f'http://localhost:3000/admin/{route}'
|
|
66
|
+
else:
|
|
67
|
+
return f'/cfg/nextjs-admin/admin/{route}'
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Client-Side (JavaScript)**: Verifies dev server availability before loading iframe:
|
|
71
|
+
|
|
72
|
+
```javascript
|
|
73
|
+
async function checkDevServerAvailable(url, retries = 3, timeout = 1000) {
|
|
74
|
+
// Tries to fetch dev server with exponential backoff
|
|
75
|
+
// - Attempt 1: immediate
|
|
76
|
+
// - Attempt 2: +500ms delay
|
|
77
|
+
// - Attempt 3: +1000ms delay
|
|
78
|
+
// Total max: ~3.5s
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Why client-side checking?**
|
|
83
|
+
- Dev servers may be compiling on first request (cold start)
|
|
84
|
+
- Browser can handle CORS/network errors better than server-side sockets
|
|
85
|
+
- Allows retry with exponential backoff for slow Next.js compilation
|
|
86
|
+
- No server-side blocking during HTML rendering
|
|
87
|
+
|
|
88
|
+
### Production Mode (DEBUG=False)
|
|
89
|
+
|
|
90
|
+
In production, both tabs serve static files:
|
|
91
|
+
- Tab 1: Serves from `/cfg/admin/admin/` (built-in static files)
|
|
92
|
+
- Tab 2: Serves from `/cfg/nextjs-admin/admin/` (extracted from ZIP)
|
|
93
|
+
|
|
94
|
+
## Fallback Mechanism
|
|
95
|
+
|
|
96
|
+
Each iframe implements a smart fallback strategy when dev servers are unavailable:
|
|
97
|
+
|
|
98
|
+
```javascript
|
|
99
|
+
function handleLoadFailure() {
|
|
100
|
+
if (!isDevMode) return; // Already using static files
|
|
101
|
+
|
|
102
|
+
const originalSrc = iframe.getAttribute('data-original-src');
|
|
103
|
+
const devUrl = new URL(originalSrc);
|
|
104
|
+
const pathPart = devUrl.pathname.replace('/admin', '');
|
|
105
|
+
|
|
106
|
+
// Determine correct fallback based on iframe ID
|
|
107
|
+
let staticUrl;
|
|
108
|
+
if (iframe.id === 'nextjs-dashboard-iframe-builtin') {
|
|
109
|
+
staticUrl = `/cfg/admin/admin/${pathPart}`;
|
|
110
|
+
} else if (iframe.id === 'nextjs-dashboard-iframe-nextjs') {
|
|
111
|
+
staticUrl = `/cfg/nextjs-admin/admin/${pathPart}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
iframe.src = staticUrl;
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Fallback Triggers
|
|
119
|
+
|
|
120
|
+
1. **Timeout**: 3 seconds without successful load
|
|
121
|
+
2. **Error Event**: iframe fails to load (network error, server down)
|
|
122
|
+
3. **Manual**: Dev server stopped during development
|
|
123
|
+
|
|
124
|
+
## iframe Communication
|
|
125
|
+
|
|
126
|
+
Each iframe maintains its own communication channel with the parent window using `postMessage`.
|
|
127
|
+
|
|
128
|
+
### Origin Detection
|
|
129
|
+
|
|
130
|
+
```javascript
|
|
131
|
+
// Get origin from data-original-src (not iframe.src, which may change)
|
|
132
|
+
const iframeSrc = iframe.getAttribute('data-original-src');
|
|
133
|
+
const iframeUrl = new URL(iframeSrc, window.location.origin);
|
|
134
|
+
const iframeOrigin = iframeUrl.origin;
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Why `data-original-src`?**
|
|
138
|
+
The `iframe.src` attribute may be changed by the fallback mechanism, so we use `data-original-src` to maintain the original intended origin for `postMessage` communication.
|
|
139
|
+
|
|
140
|
+
### Message Types
|
|
141
|
+
|
|
142
|
+
| Message Type | Direction | Purpose |
|
|
143
|
+
|--------------|-----------|---------|
|
|
144
|
+
| `parent-theme` | Parent → iframe | Sync theme (dark/light mode) |
|
|
145
|
+
| `parent-auth` | Parent → iframe | Inject JWT tokens |
|
|
146
|
+
| `iframe-ready` | iframe → Parent | iframe loaded and ready |
|
|
147
|
+
| `iframe-resize` | iframe → Parent | Update iframe height |
|
|
148
|
+
| `iframe-navigation` | iframe → Parent | Route change notification |
|
|
149
|
+
| `iframe-auth-status` | iframe → Parent | Authentication status |
|
|
150
|
+
|
|
151
|
+
## Tab Switching & State Reset
|
|
152
|
+
|
|
153
|
+
### Reset Logic
|
|
154
|
+
|
|
155
|
+
When switching tabs, the **previous** tab's iframe is reset to its initial URL:
|
|
156
|
+
|
|
157
|
+
```javascript
|
|
158
|
+
switchTab(tab) {
|
|
159
|
+
if (this.previousTab !== tab) {
|
|
160
|
+
// Reset iframe that was just hidden
|
|
161
|
+
this.resetIframe(this.previousTab);
|
|
162
|
+
this.previousTab = tab;
|
|
163
|
+
}
|
|
164
|
+
this.activeTab = tab;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
resetIframe(tab) {
|
|
168
|
+
const iframeId = tab === 'builtin'
|
|
169
|
+
? 'nextjs-dashboard-iframe-builtin'
|
|
170
|
+
: 'nextjs-dashboard-iframe-nextjs';
|
|
171
|
+
const iframe = document.getElementById(iframeId);
|
|
172
|
+
|
|
173
|
+
if (iframe) {
|
|
174
|
+
const originalSrc = iframe.getAttribute('data-original-src');
|
|
175
|
+
iframe.src = originalSrc; // Reset to initial URL
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Why Reset?**
|
|
181
|
+
This ensures that when users switch back to a tab, it starts from the home page rather than whatever route they navigated to previously.
|
|
182
|
+
|
|
183
|
+
## Static File Serving
|
|
184
|
+
|
|
185
|
+
### Built-in Admin (Tab 1)
|
|
186
|
+
|
|
187
|
+
**Location**: `django_cfg/static/frontend/admin/`
|
|
188
|
+
|
|
189
|
+
**Serving**:
|
|
190
|
+
- Development: `http://localhost:3777/admin`
|
|
191
|
+
- Production: `/cfg/admin/admin/` (Django `staticfiles`)
|
|
192
|
+
|
|
193
|
+
### External Admin (Tab 2)
|
|
194
|
+
|
|
195
|
+
**Source**: `{solution_project}/static/nextjs_admin.zip`
|
|
196
|
+
|
|
197
|
+
**Extraction**:
|
|
198
|
+
```python
|
|
199
|
+
# Target directory
|
|
200
|
+
base_dir = Path(settings.BASE_DIR) / 'static' / 'nextjs_admin'
|
|
201
|
+
|
|
202
|
+
# ZIP with metadata tracking
|
|
203
|
+
zip_path = Path(settings.BASE_DIR) / 'static' / 'nextjs_admin.zip'
|
|
204
|
+
|
|
205
|
+
# Automatic extraction with metadata comparison
|
|
206
|
+
extract_zip_if_needed(base_dir, zip_path, 'nextjs_admin')
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Serving**:
|
|
210
|
+
- Development: `http://localhost:3000/admin`
|
|
211
|
+
- Production: `/cfg/nextjs-admin/admin/` (Django serves extracted files)
|
|
212
|
+
|
|
213
|
+
### ZIP Extraction Logic
|
|
214
|
+
|
|
215
|
+
The system uses a **metadata marker file** (`.zip_meta`) to track ZIP state:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
# Metadata format: {size}:{mtime}
|
|
219
|
+
current_meta = f"{zip_stat.st_size}:{zip_stat.st_mtime}"
|
|
220
|
+
|
|
221
|
+
# Extraction triggers:
|
|
222
|
+
# 1. Directory doesn't exist → Extract
|
|
223
|
+
# 2. Marker file missing → Extract
|
|
224
|
+
# 3. ZIP metadata changed → Re-extract
|
|
225
|
+
# 4. Metadata matches → Use existing
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Why Metadata Comparison?**
|
|
229
|
+
More reliable than timestamp-only comparison, especially in Docker environments where file timestamps can be misleading.
|
|
230
|
+
|
|
231
|
+
## JWT Token Injection
|
|
232
|
+
|
|
233
|
+
Both tabs receive JWT tokens automatically for authenticated users:
|
|
234
|
+
|
|
235
|
+
```javascript
|
|
236
|
+
// Inject tokens into localStorage
|
|
237
|
+
localStorage.setItem('auth_token', '{access_token}');
|
|
238
|
+
localStorage.setItem('refresh_token', '{refresh_token}');
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Injection Strategy
|
|
242
|
+
|
|
243
|
+
1. **Server-Side**: Tokens injected into HTML response (see `views.py`)
|
|
244
|
+
2. **Client-Side**: Tokens sent via `postMessage` after iframe loads
|
|
245
|
+
3. **Storage**: Tokens stored in `localStorage` for Next.js apps
|
|
246
|
+
|
|
247
|
+
### Cache Control
|
|
248
|
+
|
|
249
|
+
HTML responses have aggressive cache-busting headers to ensure fresh token injection:
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
response['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
|
|
253
|
+
response['Pragma'] = 'no-cache'
|
|
254
|
+
response['Expires'] = '0'
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Configuration
|
|
258
|
+
|
|
259
|
+
### Django Config (solution project)
|
|
260
|
+
|
|
261
|
+
```python
|
|
262
|
+
from django_cfg import NextJsAdminConfig
|
|
263
|
+
|
|
264
|
+
config = DjangoConfig(
|
|
265
|
+
nextjs_admin=NextJsAdminConfig(
|
|
266
|
+
project_path="../django_admin/apps/admin",
|
|
267
|
+
api_output_path="src/api/generated",
|
|
268
|
+
auto_build=True,
|
|
269
|
+
# Optional overrides:
|
|
270
|
+
# static_url="/cfg/nextjs-admin/",
|
|
271
|
+
# dev_url="http://localhost:3000",
|
|
272
|
+
# tab_title="Custom Admin",
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Next.js Dev Servers
|
|
278
|
+
|
|
279
|
+
**Built-in Admin** (`django_admin/apps/admin/package.json`):
|
|
280
|
+
```json
|
|
281
|
+
{
|
|
282
|
+
"scripts": {
|
|
283
|
+
"dev": "next dev -p 3777"
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**External Admin** (solution project):
|
|
289
|
+
```json
|
|
290
|
+
{
|
|
291
|
+
"scripts": {
|
|
292
|
+
"dev": "next dev -p 3000"
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Development Workflow
|
|
298
|
+
|
|
299
|
+
### Starting Both Dev Servers
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
# Terminal 1: Built-in admin
|
|
303
|
+
cd projects/django-cfg-dev/src/django_admin/apps/admin
|
|
304
|
+
pnpm dev # Runs on port 3777
|
|
305
|
+
|
|
306
|
+
# Terminal 2: External admin
|
|
307
|
+
cd solution/projects/django_admin/apps/admin
|
|
308
|
+
pnpm dev # Runs on port 3000
|
|
309
|
+
|
|
310
|
+
# Terminal 3: Django
|
|
311
|
+
cd solution/projects/django
|
|
312
|
+
python manage.py runserver
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Running Single Tab Only
|
|
316
|
+
|
|
317
|
+
You can run only one dev server - the other will fallback to static files:
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
# Only external admin in dev mode
|
|
321
|
+
cd solution/projects/django_admin/apps/admin
|
|
322
|
+
pnpm dev
|
|
323
|
+
|
|
324
|
+
# Built-in admin will fallback to /cfg/admin/admin/
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## Template Tags Reference
|
|
328
|
+
|
|
329
|
+
### `{% nextjs_admin_url %}`
|
|
330
|
+
Returns URL for built-in dashboard (Tab 1)
|
|
331
|
+
- Dev: `http://localhost:3777/admin`
|
|
332
|
+
- Prod: `/cfg/admin/admin/`
|
|
333
|
+
|
|
334
|
+
### `{% nextjs_external_admin_url %}`
|
|
335
|
+
Returns URL for external admin (Tab 2)
|
|
336
|
+
- Dev: `http://localhost:3000/admin`
|
|
337
|
+
- Prod: `/cfg/nextjs-admin/admin/`
|
|
338
|
+
|
|
339
|
+
### `{% nextjs_external_admin_title %}`
|
|
340
|
+
Returns custom tab title from config (default: "Next.js Admin")
|
|
341
|
+
|
|
342
|
+
### `{% is_frontend_dev_mode %}`
|
|
343
|
+
Returns `True` if any dev server (port 3000 or 3777) is running
|
|
344
|
+
|
|
345
|
+
### `{% has_nextjs_external_admin %}`
|
|
346
|
+
Returns `True` if `NextJsAdminConfig` is configured
|
|
347
|
+
|
|
348
|
+
## Troubleshooting
|
|
349
|
+
|
|
350
|
+
### Tab shows wrong content
|
|
351
|
+
|
|
352
|
+
**Symptom**: Second tab shows content from first tab
|
|
353
|
+
|
|
354
|
+
**Cause**: `handleLoadFailure()` using wrong static URL
|
|
355
|
+
|
|
356
|
+
**Solution**: Check iframe ID in fallback logic (see `index.html:342-353`)
|
|
357
|
+
|
|
358
|
+
### postMessage origin mismatch
|
|
359
|
+
|
|
360
|
+
**Symptom**: Console error about mismatched origins
|
|
361
|
+
|
|
362
|
+
**Cause**: `iframeOrigin` determined from `iframe.src` after fallback changed it
|
|
363
|
+
|
|
364
|
+
**Solution**: Use `data-original-src` attribute instead (see `index.html:287`)
|
|
365
|
+
|
|
366
|
+
### Dev server not detected
|
|
367
|
+
|
|
368
|
+
**Symptom**: Tabs use static files despite dev server running
|
|
369
|
+
|
|
370
|
+
**Possible Causes**:
|
|
371
|
+
1. **Dev server still compiling**: Next.js cold start can take 1-2s
|
|
372
|
+
2. **Wrong port**: Dev server on different port than expected
|
|
373
|
+
3. **IPv6 vs IPv4**: Dev server listening only on IPv6 (`::1`)
|
|
374
|
+
4. **Firewall**: Connection blocked by firewall/antivirus
|
|
375
|
+
5. **Retry exhausted**: All 3 retry attempts failed
|
|
376
|
+
|
|
377
|
+
**Diagnostic Steps**:
|
|
378
|
+
|
|
379
|
+
1. **Check dev server is actually running**:
|
|
380
|
+
```bash
|
|
381
|
+
lsof -i :3000 -i :3777 | grep LISTEN
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
2. **Test connection manually**:
|
|
385
|
+
```bash
|
|
386
|
+
curl -I http://localhost:3000/admin
|
|
387
|
+
curl -I http://localhost:3777/admin
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
3. **Check Django template tag logs** (add temporary logging):
|
|
391
|
+
```python
|
|
392
|
+
# In django_cfg.py
|
|
393
|
+
result = _is_port_available('localhost', 3000)
|
|
394
|
+
print(f"[DEBUG] Port 3000 check: {result}")
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
4. **Increase retry attempts** (if dev server is very slow):
|
|
398
|
+
```python
|
|
399
|
+
# Temporary test - increase retries
|
|
400
|
+
_is_port_available('localhost', 3000, timeout=0.5, retries=5, retry_delay=0.1)
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
**Solutions**:
|
|
404
|
+
- **Wait for compilation**: Refresh page after 5-10 seconds
|
|
405
|
+
- **Verify port**: Check `package.json` scripts (`"dev": "next dev -p 3000"`)
|
|
406
|
+
- **Force IPv4**: Use `127.0.0.1` instead of `localhost`
|
|
407
|
+
- **Disable firewall**: Temporarily disable to test
|
|
408
|
+
- **Hard refresh**: Cmd+Shift+R to bypass browser cache
|
|
409
|
+
|
|
410
|
+
### Tokens not injected
|
|
411
|
+
|
|
412
|
+
**Symptom**: Next.js app shows "Not authenticated"
|
|
413
|
+
|
|
414
|
+
**Cause**:
|
|
415
|
+
1. User not logged in to Django
|
|
416
|
+
2. Cache returning 304 Not Modified
|
|
417
|
+
3. `postMessage` origin mismatch
|
|
418
|
+
|
|
419
|
+
**Solution**:
|
|
420
|
+
- Check Django session authentication
|
|
421
|
+
- Verify cache-control headers
|
|
422
|
+
- Check browser console for `postMessage` errors
|
|
423
|
+
|
|
424
|
+
## Performance Considerations
|
|
425
|
+
|
|
426
|
+
### Client-Side Dev Server Check
|
|
427
|
+
|
|
428
|
+
Port availability check happens in browser with exponential backoff:
|
|
429
|
+
```javascript
|
|
430
|
+
checkDevServerAvailable('http://localhost:3000', retries=3, timeout=1000)
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
**Impact per check**:
|
|
434
|
+
- **Best case**: ~0.1s (first fetch succeeds immediately)
|
|
435
|
+
- **Typical case**: ~0.5-1.5s (dev server compiling, succeeds on retry)
|
|
436
|
+
- **Worst case**: ~3.5s (all 3 attempts fail with delays: 1s + 0.5s + 1s + 1s)
|
|
437
|
+
|
|
438
|
+
**Retry schedule** (exponential backoff):
|
|
439
|
+
1. Attempt 1: immediate (0ms delay)
|
|
440
|
+
2. Attempt 2: +500ms delay
|
|
441
|
+
3. Attempt 3: +1000ms delay
|
|
442
|
+
|
|
443
|
+
**Why client-side checking?**
|
|
444
|
+
- ✅ Non-blocking: doesn't slow down server-side HTML rendering
|
|
445
|
+
- ✅ Better error handling: browser handles CORS/network errors gracefully
|
|
446
|
+
- ✅ User feedback: loading spinner shows progress
|
|
447
|
+
- ✅ Parallel loading: both iframe checks can run concurrently
|
|
448
|
+
|
|
449
|
+
### iframe Load Timeout
|
|
450
|
+
|
|
451
|
+
3-second timeout before fallback:
|
|
452
|
+
```javascript
|
|
453
|
+
loadTimeout = setTimeout(() => {
|
|
454
|
+
handleLoadFailure();
|
|
455
|
+
}, 3000);
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
**Impact**: Users may see loading spinner for up to 3s if dev server is down
|
|
459
|
+
|
|
460
|
+
### Static File Caching
|
|
461
|
+
|
|
462
|
+
- **HTML**: No caching (`no-store, no-cache`)
|
|
463
|
+
- **Assets**: Standard browser caching (JS, CSS, images)
|
|
464
|
+
|
|
465
|
+
## Security Considerations
|
|
466
|
+
|
|
467
|
+
### iframe Sandbox
|
|
468
|
+
|
|
469
|
+
Both iframes use restrictive sandbox attributes:
|
|
470
|
+
```html
|
|
471
|
+
sandbox="allow-same-origin allow-scripts allow-forms
|
|
472
|
+
allow-popups allow-modals
|
|
473
|
+
allow-storage-access-by-user-activation"
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
**Warning**: Console will show "iframe can escape sandboxing" - this is expected because `allow-same-origin` + `allow-scripts` together allow sandbox escape.
|
|
477
|
+
|
|
478
|
+
### JWT Token Security
|
|
479
|
+
|
|
480
|
+
- Tokens injected via `postMessage` with origin validation
|
|
481
|
+
- Tokens stored in `localStorage` (accessible to Next.js apps)
|
|
482
|
+
- New tokens generated on each HTML page load
|
|
483
|
+
- Tokens cleared when Django session expires
|
|
484
|
+
|
|
485
|
+
### CORS & Origins
|
|
486
|
+
|
|
487
|
+
- Dev mode: Cross-origin iframes (`localhost:3777`, `localhost:3000`)
|
|
488
|
+
- Prod mode: Same-origin iframes (both served from Django domain)
|
|
489
|
+
|
|
490
|
+
## Future Enhancements
|
|
491
|
+
|
|
492
|
+
- [ ] Configurable port allocation per project
|
|
493
|
+
- [ ] Hot-reload coordination between Django and Next.js
|
|
494
|
+
- [ ] Shared state synchronization between tabs
|
|
495
|
+
- [ ] Tab-specific URL routing (preserve state across refreshes)
|
|
496
|
+
- [ ] WebSocket support for real-time updates
|
|
497
|
+
- [ ] SSO integration for external admin panels
|
|
498
|
+
- [ ] Health check dashboard for dev servers
|
|
499
|
+
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
**Last Updated**: 2025-10-29
|
|
503
|
+
**Django-CFG Version**: 2.x
|
|
504
|
+
**Next.js Version**: 15.x
|
|
@@ -291,67 +291,17 @@
|
|
|
291
291
|
|
|
292
292
|
const iframeUrl = new URL(iframeSrc, window.location.origin);
|
|
293
293
|
const iframeOrigin = iframeUrl.origin;
|
|
294
|
-
const isDevMode = iframeOrigin !== window.location.origin;
|
|
295
294
|
|
|
296
295
|
// Debounce timer for resize events (prevents jittery iframe height changes)
|
|
297
296
|
let resizeTimer = null;
|
|
298
|
-
let loadTimeout = null;
|
|
299
297
|
|
|
300
298
|
// iframe load event
|
|
301
299
|
iframe.addEventListener('load', function() {
|
|
302
|
-
// Clear the fallback timeout
|
|
303
|
-
if (loadTimeout) {
|
|
304
|
-
clearTimeout(loadTimeout);
|
|
305
|
-
loadTimeout = null;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
300
|
setTimeout(() => {
|
|
309
301
|
sendDataToIframe();
|
|
310
302
|
}, 100);
|
|
311
303
|
});
|
|
312
304
|
|
|
313
|
-
// iframe error event - fallback to static files
|
|
314
|
-
iframe.addEventListener('error', function() {
|
|
315
|
-
console.warn('[Django-CFG] iframe failed to load, falling back to static files');
|
|
316
|
-
handleLoadFailure();
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
// Timeout fallback - if iframe doesn't load in 3 seconds
|
|
320
|
-
loadTimeout = setTimeout(() => {
|
|
321
|
-
if (!iframe.classList.contains('loaded') && isDevMode) {
|
|
322
|
-
console.warn('[Django-CFG] Dev server timeout, falling back to static files');
|
|
323
|
-
handleLoadFailure();
|
|
324
|
-
} else if (!iframe.classList.contains('loaded')) {
|
|
325
|
-
console.log('[Django-CFG] Timeout - showing iframe anyway');
|
|
326
|
-
if (loading) loading.classList.add('hidden');
|
|
327
|
-
iframe.classList.add('loaded');
|
|
328
|
-
}
|
|
329
|
-
}, 3000);
|
|
330
|
-
|
|
331
|
-
// Handle load failure - switch to static files
|
|
332
|
-
function handleLoadFailure() {
|
|
333
|
-
if (!isDevMode) return; // Already using static files
|
|
334
|
-
|
|
335
|
-
const originalSrc = iframe.getAttribute('data-original-src');
|
|
336
|
-
if (!originalSrc) return;
|
|
337
|
-
|
|
338
|
-
// Extract path from dev server URL
|
|
339
|
-
const devUrl = new URL(originalSrc);
|
|
340
|
-
const pathPart = devUrl.pathname.replace('/admin', '').replace(/^\//, '');
|
|
341
|
-
|
|
342
|
-
// Build static files URL
|
|
343
|
-
const staticUrl = `/cfg/admin/admin/${pathPart}`;
|
|
344
|
-
|
|
345
|
-
console.log('[Django-CFG] Switching to static files:', staticUrl);
|
|
346
|
-
iframe.src = staticUrl;
|
|
347
|
-
|
|
348
|
-
// Show iframe after switching
|
|
349
|
-
setTimeout(() => {
|
|
350
|
-
if (loading) loading.classList.add('hidden');
|
|
351
|
-
iframe.classList.add('loaded');
|
|
352
|
-
}, 500);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
305
|
// Send theme and auth data to iframe
|
|
356
306
|
function sendDataToIframe() {
|
|
357
307
|
if (!iframe || !iframe.contentWindow) {
|
|
@@ -14,29 +14,53 @@ from rest_framework_simplejwt.tokens import RefreshToken
|
|
|
14
14
|
register = template.Library()
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
def _is_port_available(host: str, port: int, timeout: float = 0.1) -> bool:
|
|
17
|
+
def _is_port_available(host: str, port: int, timeout: float = 0.1, retries: int = 3, retry_delay: float = 0.05) -> bool:
|
|
18
18
|
"""
|
|
19
|
-
Check if a port is available (listening) on the specified host.
|
|
19
|
+
Check if a port is available (listening) on the specified host with retry logic.
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
Performs multiple connection attempts with delays to handle dev servers
|
|
22
|
+
that may be compiling or starting up (e.g., Next.js first request).
|
|
23
23
|
|
|
24
24
|
Args:
|
|
25
25
|
host: Host to check (e.g., 'localhost', '127.0.0.1')
|
|
26
26
|
port: Port number to check
|
|
27
|
-
timeout: Connection timeout in seconds (default: 0.1s)
|
|
27
|
+
timeout: Connection timeout in seconds per attempt (default: 0.1s)
|
|
28
|
+
retries: Number of retry attempts (default: 3)
|
|
29
|
+
retry_delay: Delay between retries in seconds (default: 0.05s)
|
|
28
30
|
|
|
29
31
|
Returns:
|
|
30
32
|
bool: True if port is available, False otherwise
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
# Quick check (default): ~0.4s max (3 attempts × 0.1s + 2 × 0.05s delay)
|
|
36
|
+
_is_port_available('localhost', 3000)
|
|
37
|
+
|
|
38
|
+
# Patient check for slow servers
|
|
39
|
+
_is_port_available('localhost', 3000, timeout=0.5, retries=5)
|
|
31
40
|
"""
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
import time
|
|
42
|
+
|
|
43
|
+
for attempt in range(retries):
|
|
44
|
+
try:
|
|
45
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
46
|
+
sock.settimeout(timeout)
|
|
47
|
+
result = sock.connect_ex((host, port))
|
|
48
|
+
sock.close()
|
|
49
|
+
|
|
50
|
+
if result == 0:
|
|
51
|
+
return True # Success - port is available
|
|
52
|
+
|
|
53
|
+
# Port not available, retry if not last attempt
|
|
54
|
+
if attempt < retries - 1:
|
|
55
|
+
time.sleep(retry_delay)
|
|
56
|
+
|
|
57
|
+
except Exception:
|
|
58
|
+
# Connection error, retry if not last attempt
|
|
59
|
+
if attempt < retries - 1:
|
|
60
|
+
time.sleep(retry_delay)
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
return False # All retries failed
|
|
40
64
|
|
|
41
65
|
|
|
42
66
|
@register.simple_tag
|
|
@@ -169,9 +193,12 @@ def nextjs_admin_url(path=''):
|
|
|
169
193
|
"""
|
|
170
194
|
Get the URL for Next.js Admin Panel (Built-in Dashboard - Tab 1).
|
|
171
195
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
196
|
+
In DEBUG mode, always returns dev server URL. Client-side JavaScript
|
|
197
|
+
will handle fallback to static files if dev server is unavailable.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
- DEBUG=True: http://localhost:3777/admin/{path}
|
|
201
|
+
- DEBUG=False: /cfg/admin/admin/{path}
|
|
175
202
|
|
|
176
203
|
Note: Port 3000 is reserved for external Next.js admin (Tab 2).
|
|
177
204
|
Both tabs use /admin route for consistency.
|
|
@@ -188,12 +215,12 @@ def nextjs_admin_url(path=''):
|
|
|
188
215
|
# Production mode: always use static files with /admin route
|
|
189
216
|
return f'/cfg/admin/admin/{path}' if path else '/cfg/admin/admin/'
|
|
190
217
|
|
|
191
|
-
# Check if port
|
|
192
|
-
|
|
218
|
+
# Check if port 3777 is available for Tab 1 (built-in admin)
|
|
219
|
+
port_3777_available = _is_port_available('localhost', 3777)
|
|
193
220
|
|
|
194
|
-
if
|
|
195
|
-
# Dev server is running on
|
|
196
|
-
base_url = 'http://localhost:
|
|
221
|
+
if port_3777_available:
|
|
222
|
+
# Dev server is running on 3777 - use /admin route for consistency
|
|
223
|
+
base_url = 'http://localhost:3777/admin'
|
|
197
224
|
return f'{base_url}/{path}' if path else base_url
|
|
198
225
|
else:
|
|
199
226
|
# No dev server - use static files with /admin route
|
|
@@ -207,7 +234,7 @@ def is_frontend_dev_mode():
|
|
|
207
234
|
|
|
208
235
|
Auto-detects by checking:
|
|
209
236
|
- DEBUG=True
|
|
210
|
-
- AND (port 3000 OR port
|
|
237
|
+
- AND (port 3000 OR port 3777 is available)
|
|
211
238
|
|
|
212
239
|
Returns True if any Next.js dev server is detected.
|
|
213
240
|
|
|
@@ -222,7 +249,7 @@ def is_frontend_dev_mode():
|
|
|
222
249
|
|
|
223
250
|
# Check if either dev server is running
|
|
224
251
|
return (_is_port_available('localhost', 3000) or
|
|
225
|
-
_is_port_available('localhost',
|
|
252
|
+
_is_port_available('localhost', 3777))
|
|
226
253
|
|
|
227
254
|
|
|
228
255
|
@register.simple_tag
|
|
@@ -252,12 +279,14 @@ def nextjs_external_admin_url(route=''):
|
|
|
252
279
|
"""
|
|
253
280
|
Get URL for external Next.js admin (Tab 2 - solution project).
|
|
254
281
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
282
|
+
In DEBUG mode, always returns dev server URL. Client-side JavaScript
|
|
283
|
+
will handle fallback to static files if dev server is unavailable.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
- DEBUG=True: http://localhost:3000/admin/{route}
|
|
287
|
+
- DEBUG=False: /cfg/nextjs-admin/admin/{route}
|
|
258
288
|
|
|
259
289
|
This is for the external admin panel (solution project).
|
|
260
|
-
No env variable needed - automatically detects running dev server!
|
|
261
290
|
|
|
262
291
|
Usage in template:
|
|
263
292
|
{% load django_cfg %}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-cfg
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.103
|
|
4
4
|
Summary: Modern Django framework with type-safe Pydantic v2 configuration, Next.js admin integration, real-time WebSockets, and 8 enterprise apps. Replace settings.py with validated models, 90% less code. Production-ready with AI agents, auto-generated TypeScript clients, and zero-config features.
|
|
5
5
|
Project-URL: Homepage, https://djangocfg.com
|
|
6
6
|
Project-URL: Documentation, https://djangocfg.com
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
django_cfg/README.md,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
django_cfg/__init__.py,sha256
|
|
2
|
+
django_cfg/__init__.py,sha256=NcoABp5CJlspxHWWGZJqyOKUZB7Ny0hs794hP123d_Y,1621
|
|
3
3
|
django_cfg/apps.py,sha256=72m3uuvyqGiLx6gOfE-BD3P61jddCCERuBOYpxTX518,1605
|
|
4
4
|
django_cfg/config.py,sha256=y4Z3rnYsHBE0TehpwAIPaxr---mkvyKrZGGsNwYso74,1398
|
|
5
5
|
django_cfg/apps/__init__.py,sha256=JtDmEYt1OcleWM2ZaeX0LKDnRQzPOavfaXBWG4ECB5Q,26
|
|
@@ -664,7 +664,7 @@ django_cfg/models/base/module.py,sha256=nxN1Y9J4l94kOfSXLQJ2eGgIGWTq8kyh7hUGvCQN
|
|
|
664
664
|
django_cfg/models/django/__init__.py,sha256=xY_ts1oGmMROXfKHLuERqwG35dQf3EpsZxqj9DcRYwI,397
|
|
665
665
|
django_cfg/models/django/axes.py,sha256=-4nk2gSfpj7lNY5vnm_2jHVLz8VAKoEd9yF2TuCR8O8,5624
|
|
666
666
|
django_cfg/models/django/constance.py,sha256=E4U3HS_gFq5_q2nhI1R8inONGtD7IgvSwxOEGNGGrEI,9127
|
|
667
|
-
django_cfg/models/django/crypto_fields.py,sha256=
|
|
667
|
+
django_cfg/models/django/crypto_fields.py,sha256=EritHX2V8q5zHoVznGIO0sX78GPYif6NQDS_CiXvSKo,6238
|
|
668
668
|
django_cfg/models/django/environment.py,sha256=lBCHBs1lphv9tlu1BCTfLZeH_kUame0p66A_BIjBY7M,9440
|
|
669
669
|
django_cfg/models/django/openapi.py,sha256=avE3iapaCj8eyOqVUum_v2EExR3V-hwHrexqtXMHtTQ,3739
|
|
670
670
|
django_cfg/models/django/revolution_legacy.py,sha256=Z4SPUS7QSv62EuPAeFFoXGEgqLmdXnVEr7Ofk1IDtVc,8918
|
|
@@ -859,7 +859,7 @@ django_cfg/modules/django_client/core/validation/rules/base.py,sha256=xVJli0eSEz
|
|
|
859
859
|
django_cfg/modules/django_client/core/validation/rules/type_hints.py,sha256=hwjTMADillsTPruDvXZQeZMj4LVV443zxY9o0Gqgg6k,10200
|
|
860
860
|
django_cfg/modules/django_client/management/__init__.py,sha256=mCTPP_bIOmqNnn0WAG2n4BuF6zwc9PTgdZr_dORfNDk,54
|
|
861
861
|
django_cfg/modules/django_client/management/commands/__init__.py,sha256=CJ55pHUNYQ5h-QHUe3axeTtxzlUJv7wbEuZmGN21iCM,36
|
|
862
|
-
django_cfg/modules/django_client/management/commands/generate_client.py,sha256=
|
|
862
|
+
django_cfg/modules/django_client/management/commands/generate_client.py,sha256=JShicezcxeUJ8_5l5HJtINbDwsdhoK_4gljyaPlfXGM,30910
|
|
863
863
|
django_cfg/modules/django_client/management/commands/validate_openapi.py,sha256=IBKk7oRP3tMQzXjvZNIgQtMPk3k_mB2diNS7bkaSLz4,11011
|
|
864
864
|
django_cfg/modules/django_client/spectacular/__init__.py,sha256=M8fG-odu2ltkG36aMMr0KDkCKGX676TwdrJO8vky2cI,345
|
|
865
865
|
django_cfg/modules/django_client/spectacular/async_detection.py,sha256=S_pwGR7_2SIWHjZJyiu7SCfySF3Nr3P8eqjDyBSkkLs,5731
|
|
@@ -1023,7 +1023,7 @@ django_cfg/modules/nextjs_admin/apps.py,sha256=HxVUMmWTKdYpwJ00iIfWVFsBzsawsOVhE
|
|
|
1023
1023
|
django_cfg/modules/nextjs_admin/urls.py,sha256=7n0yStm0WNchw14Rtu_mgsIA3WKQsYP9WZt3-YOUWjU,603
|
|
1024
1024
|
django_cfg/modules/nextjs_admin/views.py,sha256=SELkdq6ioNqFLInDKfYyRSZRaXRtuGAVjQfE8sACQ_Q,10382
|
|
1025
1025
|
django_cfg/modules/nextjs_admin/models/__init__.py,sha256=WGw9KXcYd1O9AoA_bpMoz2gLZUlRzjGmUBjjbObcUi0,100
|
|
1026
|
-
django_cfg/modules/nextjs_admin/models/config.py,sha256=
|
|
1026
|
+
django_cfg/modules/nextjs_admin/models/config.py,sha256=75iM81aaTlDQdhfvdlCzj6o9zD5JIK522CIVzM-GSvE,6326
|
|
1027
1027
|
django_cfg/modules/nextjs_admin/templatetags/__init__.py,sha256=ChVBnJggCIY8rMhfyJFoA8k0qKo-8FtJknrk54Vx4wM,51
|
|
1028
1028
|
django_cfg/modules/nextjs_admin/templatetags/nextjs_admin.py,sha256=aAekrlu3pvvx3I4uJGT3S2ie8QfF94umDBjgAF71EII,4483
|
|
1029
1029
|
django_cfg/registry/__init__.py,sha256=CaiL9KwqPzXlIe5-4Qr7PQu5ZxAW1HtuDIXZ7-ktRQg,538
|
|
@@ -1072,11 +1072,12 @@ django_cfg/static/js/api/support/index.mjs,sha256=oPA3iGkUWYyKQuJlI5-tSxD3AOhwlA
|
|
|
1072
1072
|
django_cfg/static/js/api/tasks/client.mjs,sha256=tIy8K-finXzTUL9kOo_L4Q1kchDaHyuzjwS4VymiWPM,3579
|
|
1073
1073
|
django_cfg/static/js/api/tasks/index.mjs,sha256=yCY1GzdD-RtFZ3pAfk1l0msgO1epyo0lsGCjH0g1Afc,294
|
|
1074
1074
|
django_cfg/templates/__init__.py,sha256=IzLjt-a7VIJ0OutmAE1_-w0_LpL2u0MgGpnIabjZuW8,19
|
|
1075
|
-
django_cfg/templates/admin/
|
|
1075
|
+
django_cfg/templates/admin/DUAL_TAB_ARCHITECTURE.md,sha256=kDUOgLjEv6OdtZNadtQuPhNQmeEZ_TVNt6PKGF6abw4,15607
|
|
1076
|
+
django_cfg/templates/admin/index.html,sha256=ygX2GklrPx9yEzk507Kk7Z56zDfaxjMHpq-TqQCN_1M,17743
|
|
1076
1077
|
django_cfg/templates/emails/base_email.html,sha256=TWcvYa2IHShlF_E8jf1bWZStRO0v8G4L_GexPxvz6XQ,8836
|
|
1077
1078
|
django_cfg/templates/unfold/layouts/skeleton.html,sha256=2ArkcNZ34mFs30cOAsTQ1EZiDXcB0aVxkO71lJq9SLE,718
|
|
1078
1079
|
django_cfg/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
1079
|
-
django_cfg/templatetags/django_cfg.py,sha256=
|
|
1080
|
+
django_cfg/templatetags/django_cfg.py,sha256=bJPLZ9Uwlr499-umGN7bcaUibMgZs_XQHvoee93fMfk,10486
|
|
1080
1081
|
django_cfg/utils/__init__.py,sha256=64wwXJuXytvwt8Ze_erSR2HmV07nGWJ6DV5wloRBvYE,435
|
|
1081
1082
|
django_cfg/utils/path_resolution.py,sha256=2n0I04lQkSssFaELu3A93YyMAl1K10KPdpxMt5k4Iy0,13341
|
|
1082
1083
|
django_cfg/utils/smart_defaults.py,sha256=ZUj6K_Deq-fp5O0Dy_Emt257UWFn0f9bkgFv9YCR58U,9239
|
|
@@ -1084,9 +1085,9 @@ django_cfg/utils/version_check.py,sha256=WO51J2m2e-wVqWCRwbultEwu3q1lQasV67Mw2aa
|
|
|
1084
1085
|
django_cfg/CHANGELOG.md,sha256=jtT3EprqEJkqSUh7IraP73vQ8PmKUMdRtznQsEnqDZk,2052
|
|
1085
1086
|
django_cfg/CONTRIBUTING.md,sha256=DU2kyQ6PU0Z24ob7O_OqKWEYHcZmJDgzw-lQCmu6uBg,3041
|
|
1086
1087
|
django_cfg/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
|
|
1087
|
-
django_cfg/pyproject.toml,sha256=
|
|
1088
|
-
django_cfg-1.4.
|
|
1089
|
-
django_cfg-1.4.
|
|
1090
|
-
django_cfg-1.4.
|
|
1091
|
-
django_cfg-1.4.
|
|
1092
|
-
django_cfg-1.4.
|
|
1088
|
+
django_cfg/pyproject.toml,sha256=Z7stNPVSGKgzrgQe7wcp-wgZA9lz420IiQ2KoZSNcso,8573
|
|
1089
|
+
django_cfg-1.4.103.dist-info/METADATA,sha256=LAiU0BRYKRV1qpQVNFUSQKKBalgW4vsrSytQEoJ9T1w,23734
|
|
1090
|
+
django_cfg-1.4.103.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
1091
|
+
django_cfg-1.4.103.dist-info/entry_points.txt,sha256=Ucmde4Z2wEzgb4AggxxZ0zaYDb9HpyE5blM3uJ0_VNg,56
|
|
1092
|
+
django_cfg-1.4.103.dist-info/licenses/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
|
|
1093
|
+
django_cfg-1.4.103.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|