django-cfg 1.4.104__py3-none-any.whl → 1.4.106__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/core/generation/core_generators/templates.py +38 -23
- django_cfg/modules/django_admin/IMPORT_EXPORT_FIX.md +72 -0
- django_cfg/modules/django_admin/base/pydantic_admin.py +71 -7
- django_cfg/modules/django_import_export/README.md +19 -1
- django_cfg/modules/django_import_export/__init__.py +8 -0
- django_cfg/modules/django_import_export/apps.py +16 -0
- django_cfg/modules/django_import_export/templates/admin/import_export/change_list_export.html +6 -0
- django_cfg/modules/django_import_export/templates/admin/import_export/change_list_export_item.html +7 -0
- django_cfg/modules/django_import_export/templates/admin/import_export/change_list_import.html +6 -0
- django_cfg/modules/django_import_export/templates/admin/import_export/change_list_import_export.html +39 -0
- django_cfg/modules/django_import_export/templates/admin/import_export/change_list_import_item.html +7 -0
- django_cfg/pyproject.toml +1 -1
- django_cfg/templates/admin/DUAL_TAB_ARCHITECTURE.md +57 -0
- django_cfg/templates/admin/index.html +60 -7
- {django_cfg-1.4.104.dist-info → django_cfg-1.4.106.dist-info}/METADATA +1 -1
- {django_cfg-1.4.104.dist-info → django_cfg-1.4.106.dist-info}/RECORD +20 -13
- {django_cfg-1.4.104.dist-info → django_cfg-1.4.106.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.104.dist-info → django_cfg-1.4.106.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.104.dist-info → django_cfg-1.4.106.dist-info}/licenses/LICENSE +0 -0
django_cfg/__init__.py
CHANGED
|
@@ -96,42 +96,57 @@ class TemplateSettingsGenerator:
|
|
|
96
96
|
|
|
97
97
|
def _discover_app_templates(self) -> List[Path]:
|
|
98
98
|
"""
|
|
99
|
-
Auto-discover template directories from django-cfg apps.
|
|
99
|
+
Auto-discover template directories from django-cfg apps and modules.
|
|
100
100
|
|
|
101
101
|
Looks for:
|
|
102
102
|
- app/templates/
|
|
103
103
|
- app/admin_interface/templates/
|
|
104
104
|
- app/frontend/templates/
|
|
105
|
+
- modules/*/templates/
|
|
105
106
|
|
|
106
107
|
Returns:
|
|
107
108
|
List of discovered template directory paths
|
|
108
109
|
"""
|
|
109
110
|
app_templates = []
|
|
110
111
|
|
|
111
|
-
# Find django-cfg
|
|
112
|
+
# Find django-cfg directory
|
|
112
113
|
django_cfg_dir = Path(__file__).parent.parent.parent.parent
|
|
113
|
-
apps_dir = django_cfg_dir / 'apps'
|
|
114
|
-
|
|
115
|
-
if not apps_dir.exists():
|
|
116
|
-
return app_templates
|
|
117
|
-
|
|
118
|
-
# Scan each app directory
|
|
119
|
-
for app_dir in apps_dir.iterdir():
|
|
120
|
-
if not app_dir.is_dir():
|
|
121
|
-
continue
|
|
122
|
-
|
|
123
|
-
# Skip special directories
|
|
124
|
-
if app_dir.name.startswith(('@', '_', '.')):
|
|
125
|
-
continue
|
|
126
114
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
115
|
+
# Scan apps directory
|
|
116
|
+
apps_dir = django_cfg_dir / 'apps'
|
|
117
|
+
if apps_dir.exists():
|
|
118
|
+
for app_dir in apps_dir.iterdir():
|
|
119
|
+
if not app_dir.is_dir():
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
# Skip special directories
|
|
123
|
+
if app_dir.name.startswith(('@', '_', '.')):
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
# Look for common template directory patterns
|
|
127
|
+
possible_template_dirs = [
|
|
128
|
+
app_dir / 'templates',
|
|
129
|
+
app_dir / 'admin_interface' / 'templates',
|
|
130
|
+
app_dir / 'frontend' / 'templates',
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
for template_dir in possible_template_dirs:
|
|
134
|
+
if template_dir.exists() and template_dir.is_dir():
|
|
135
|
+
app_templates.append(template_dir)
|
|
136
|
+
|
|
137
|
+
# Scan modules directory for templates
|
|
138
|
+
modules_dir = django_cfg_dir / 'modules'
|
|
139
|
+
if modules_dir.exists():
|
|
140
|
+
for module_dir in modules_dir.iterdir():
|
|
141
|
+
if not module_dir.is_dir():
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
# Skip special directories
|
|
145
|
+
if module_dir.name.startswith(('@', '_', '.')):
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
# Check if module has templates directory
|
|
149
|
+
template_dir = module_dir / 'templates'
|
|
135
150
|
if template_dir.exists() and template_dir.is_dir():
|
|
136
151
|
app_templates.append(template_dir)
|
|
137
152
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Import/Export Button Fix
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
|
|
5
|
+
Import/Export buttons in `PydanticAdmin` were displaying incorrectly because:
|
|
6
|
+
|
|
7
|
+
1. `PydanticAdmin` was using the original `ImportExportModelAdmin` from django-import-export
|
|
8
|
+
2. Original django-import-export templates don't integrate properly with Unfold UI
|
|
9
|
+
3. Buttons were styled as tabs instead of toolbar buttons
|
|
10
|
+
|
|
11
|
+
## Solution
|
|
12
|
+
|
|
13
|
+
Updated `PydanticAdmin` base class to use django_cfg's custom `ImportExportMixin`:
|
|
14
|
+
|
|
15
|
+
### Before
|
|
16
|
+
```python
|
|
17
|
+
class UnfoldImportExportModelAdmin(ImportExportModelAdmin, UnfoldModelAdmin):
|
|
18
|
+
pass
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### After
|
|
22
|
+
```python
|
|
23
|
+
class UnfoldImportExportModelAdmin(UnfoldModelAdmin, ImportExportMixin):
|
|
24
|
+
pass
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Changes Made
|
|
28
|
+
|
|
29
|
+
1. **MRO (Method Resolution Order)**:
|
|
30
|
+
- `UnfoldModelAdmin` comes first (for template priority)
|
|
31
|
+
- `ImportExportMixin` comes second (adds import/export functionality)
|
|
32
|
+
|
|
33
|
+
2. **Custom Templates** (`django_cfg.modules.django_import_export`):
|
|
34
|
+
- Custom `change_list_*.html` templates that extend Unfold's base
|
|
35
|
+
- Properly styled import/export buttons as round icons
|
|
36
|
+
- Import button: green with upload icon
|
|
37
|
+
- Export button: blue with download icon
|
|
38
|
+
|
|
39
|
+
3. **Integration**:
|
|
40
|
+
- Uses Unfold's forms (`ImportForm`, `ExportForm`)
|
|
41
|
+
- Buttons appear in `object-tools-items` block (next to "Add" button)
|
|
42
|
+
- Full dark mode support
|
|
43
|
+
|
|
44
|
+
## Result
|
|
45
|
+
|
|
46
|
+
Now `PydanticAdmin` admins with `import_export_enabled=True` will have:
|
|
47
|
+
- ✅ Properly styled import/export buttons
|
|
48
|
+
- ✅ Consistent Unfold UI
|
|
49
|
+
- ✅ Round icon buttons matching the design system
|
|
50
|
+
- ✅ Correct positioning in the toolbar
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
No changes required in user code - this fix is automatic:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from django_cfg.modules.django_admin import AdminConfig
|
|
58
|
+
from django_cfg.modules.django_admin.base import PydanticAdmin
|
|
59
|
+
|
|
60
|
+
config = AdminConfig(
|
|
61
|
+
model=MyModel,
|
|
62
|
+
import_export_enabled=True,
|
|
63
|
+
resource_class=MyModelResource,
|
|
64
|
+
list_display=["name", "status"],
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@admin.register(MyModel)
|
|
68
|
+
class MyModelAdmin(PydanticAdmin):
|
|
69
|
+
config = config
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The import/export buttons will now display correctly!
|
|
@@ -24,17 +24,24 @@ def _get_base_admin_class():
|
|
|
24
24
|
|
|
25
25
|
MRO (Method Resolution Order):
|
|
26
26
|
UnfoldImportExportModelAdmin
|
|
27
|
-
└─
|
|
28
|
-
└─
|
|
27
|
+
└─ UnfoldModelAdmin # Unfold UI (first for template priority)
|
|
28
|
+
└─ ImportExportMixin # Import/Export functionality (django_cfg custom)
|
|
29
29
|
└─ Django ModelAdmin
|
|
30
30
|
|
|
31
31
|
This ensures both Unfold UI and Import/Export work together seamlessly.
|
|
32
|
+
|
|
33
|
+
Uses django_cfg's custom ImportExportMixin which includes:
|
|
34
|
+
- Custom templates for proper Unfold styling
|
|
35
|
+
- Unfold forms (ImportForm, ExportForm)
|
|
36
|
+
- Properly styled import/export buttons
|
|
32
37
|
"""
|
|
33
|
-
|
|
38
|
+
# Use original ImportExportModelAdmin with Unfold
|
|
39
|
+
from import_export.admin import ImportExportModelAdmin as BaseImportExportModelAdmin
|
|
34
40
|
from unfold.admin import ModelAdmin as UnfoldModelAdmin
|
|
35
41
|
|
|
36
|
-
class UnfoldImportExportModelAdmin(
|
|
37
|
-
"""Combined
|
|
42
|
+
class UnfoldImportExportModelAdmin(BaseImportExportModelAdmin, UnfoldModelAdmin):
|
|
43
|
+
"""Combined Import/Export + Unfold admin base class."""
|
|
44
|
+
# Import/Export should come FIRST in MRO to get its get_urls() method
|
|
38
45
|
pass
|
|
39
46
|
|
|
40
47
|
return UnfoldImportExportModelAdmin
|
|
@@ -122,8 +129,65 @@ class PydanticAdminMixin:
|
|
|
122
129
|
cls.preserve_filters = config.preserve_filters
|
|
123
130
|
|
|
124
131
|
# Import/Export configuration
|
|
125
|
-
if config.import_export_enabled
|
|
126
|
-
|
|
132
|
+
if config.import_export_enabled:
|
|
133
|
+
# Set import/export template
|
|
134
|
+
cls.change_list_template = 'admin/import_export/change_list_import_export.html'
|
|
135
|
+
|
|
136
|
+
if config.resource_class:
|
|
137
|
+
# Use provided resource class
|
|
138
|
+
cls.resource_class = config.resource_class
|
|
139
|
+
else:
|
|
140
|
+
# Auto-generate resource class
|
|
141
|
+
cls.resource_class = cls._generate_resource_class(config)
|
|
142
|
+
|
|
143
|
+
# Override changelist_view to add import/export context
|
|
144
|
+
original_changelist_view = cls.changelist_view
|
|
145
|
+
|
|
146
|
+
def changelist_view_with_import_export(self, request, extra_context=None):
|
|
147
|
+
if extra_context is None:
|
|
148
|
+
extra_context = {}
|
|
149
|
+
extra_context['has_import_permission'] = self.has_import_permission(request)
|
|
150
|
+
extra_context['has_export_permission'] = self.has_export_permission(request)
|
|
151
|
+
return original_changelist_view(self, request, extra_context)
|
|
152
|
+
|
|
153
|
+
cls.changelist_view = changelist_view_with_import_export
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def _generate_resource_class(cls, config: AdminConfig):
|
|
157
|
+
"""Auto-generate a ModelResource class for import/export."""
|
|
158
|
+
from import_export import resources
|
|
159
|
+
|
|
160
|
+
target_model = config.model
|
|
161
|
+
|
|
162
|
+
# Get all model fields
|
|
163
|
+
model_fields = []
|
|
164
|
+
for field in target_model._meta.get_fields():
|
|
165
|
+
# Skip relations and auto fields that shouldn't be imported
|
|
166
|
+
if field.concrete and not field.many_to_many:
|
|
167
|
+
# Skip password fields for security
|
|
168
|
+
if 'password' not in field.name.lower():
|
|
169
|
+
model_fields.append(field.name)
|
|
170
|
+
|
|
171
|
+
# Create dynamic resource class with explicit Meta attributes
|
|
172
|
+
meta_attrs = {
|
|
173
|
+
'model': target_model,
|
|
174
|
+
'fields': tuple(model_fields),
|
|
175
|
+
'import_id_fields': ['id'] if 'id' in model_fields else [],
|
|
176
|
+
'skip_unchanged': True,
|
|
177
|
+
'report_skipped': True,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
# Create Meta class
|
|
181
|
+
ResourceMeta = type('Meta', (), meta_attrs)
|
|
182
|
+
|
|
183
|
+
# Create Resource class
|
|
184
|
+
AutoGeneratedResource = type(
|
|
185
|
+
f'{target_model.__name__}Resource',
|
|
186
|
+
(resources.ModelResource,),
|
|
187
|
+
{'Meta': ResourceMeta}
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return AutoGeneratedResource
|
|
127
191
|
|
|
128
192
|
@classmethod
|
|
129
193
|
def _build_list_display(cls, config: AdminConfig) -> List[str]:
|
|
@@ -74,8 +74,26 @@ The module automatically works with:
|
|
|
74
74
|
- Django-cfg configuration system
|
|
75
75
|
- All original django-import-export features
|
|
76
76
|
|
|
77
|
+
## Custom Templates
|
|
78
|
+
|
|
79
|
+
This module includes custom templates for proper Unfold integration:
|
|
80
|
+
|
|
81
|
+
### Button Styling
|
|
82
|
+
- Import/Export buttons are styled as round icon buttons matching Unfold's design
|
|
83
|
+
- Import button: Green with upload icon
|
|
84
|
+
- Export button: Blue with download icon
|
|
85
|
+
- Positioned next to the "Add" button in the admin interface
|
|
86
|
+
|
|
87
|
+
### Template Locations
|
|
88
|
+
Templates are located in `templates/admin/import_export/`:
|
|
89
|
+
- `change_list_import_export.html` - For both import and export
|
|
90
|
+
- `change_list_import.html` - Import only
|
|
91
|
+
- `change_list_export.html` - Export only
|
|
92
|
+
- `change_list_import_item.html` - Import button template
|
|
93
|
+
- `change_list_export_item.html` - Export button template
|
|
94
|
+
|
|
77
95
|
## Full Documentation
|
|
78
96
|
|
|
79
97
|
For complete documentation, see the official [django-import-export docs](https://django-import-export.readthedocs.io/).
|
|
80
98
|
|
|
81
|
-
This module adds
|
|
99
|
+
This module adds custom Unfold-styled templates for better visual integration with the admin interface.
|
|
@@ -22,6 +22,14 @@ class ImportExportMixin(BaseImportExportMixin):
|
|
|
22
22
|
import_form_class = ImportForm
|
|
23
23
|
export_form_class = ExportForm
|
|
24
24
|
|
|
25
|
+
def changelist_view(self, request, extra_context=None):
|
|
26
|
+
"""Add import/export permissions to context."""
|
|
27
|
+
if extra_context is None:
|
|
28
|
+
extra_context = {}
|
|
29
|
+
extra_context['has_import_permission'] = self.has_import_permission(request)
|
|
30
|
+
extra_context['has_export_permission'] = self.has_export_permission(request)
|
|
31
|
+
return super().changelist_view(request, extra_context)
|
|
32
|
+
|
|
25
33
|
|
|
26
34
|
class ImportExportModelAdmin(BaseImportExportModelAdmin):
|
|
27
35
|
"""Django-CFG enhanced ImportExportModelAdmin with custom templates and Unfold forms."""
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from django.apps import AppConfig
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DjangoImportExportConfig(AppConfig):
|
|
5
|
+
"""Django Import/Export integration app configuration."""
|
|
6
|
+
|
|
7
|
+
default_auto_field = 'django.db.models.BigAutoField'
|
|
8
|
+
name = 'django_cfg.modules.django_import_export'
|
|
9
|
+
label = 'django_cfg_import_export'
|
|
10
|
+
verbose_name = 'Django Import/Export Integration'
|
|
11
|
+
|
|
12
|
+
def ready(self):
|
|
13
|
+
"""Initialize module classes when Django is ready."""
|
|
14
|
+
# Import and setup classes here to avoid AppRegistryNotReady
|
|
15
|
+
from . import _setup_classes
|
|
16
|
+
_setup_classes()
|
django_cfg/modules/django_import_export/templates/admin/import_export/change_list_export_item.html
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{% load admin_urls i18n %}
|
|
2
|
+
|
|
3
|
+
{% if has_export_permission %}
|
|
4
|
+
<a href="export/{{ cl.get_query_string }}" class="bg-blue-600 flex items-center h-9 justify-center -my-1 rounded-full w-9 hover:bg-blue-700" title="{% trans 'Export' %}">
|
|
5
|
+
<span class="material-symbols-outlined text-white">download</span>
|
|
6
|
+
</a>
|
|
7
|
+
{% endif %}
|
django_cfg/modules/django_import_export/templates/admin/import_export/change_list_import_export.html
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{% extends "admin/change_list.html" %}
|
|
2
|
+
|
|
3
|
+
{% block extrahead %}
|
|
4
|
+
{{ block.super }}
|
|
5
|
+
<style>
|
|
6
|
+
/* Object Tools - Admin toolbar buttons */
|
|
7
|
+
.object-tools {
|
|
8
|
+
display: flex !important;
|
|
9
|
+
gap: 1rem !important; /* 16px gap between groups */
|
|
10
|
+
align-items: center !important;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* Remove extra spacing from individual object-tools items */
|
|
14
|
+
.object-tools > * {
|
|
15
|
+
margin: 0 !important;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* Import/Export button group */
|
|
19
|
+
.import-export-group {
|
|
20
|
+
display: flex !important;
|
|
21
|
+
gap: 0.5rem !important; /* 8px gap between import/export buttons */
|
|
22
|
+
align-items: center !important;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* Separator before import/export group */
|
|
26
|
+
.import-export-separator {
|
|
27
|
+
margin: 0 0.5rem !important; /* 8px margin on each side */
|
|
28
|
+
}
|
|
29
|
+
</style>
|
|
30
|
+
{% endblock %}
|
|
31
|
+
|
|
32
|
+
{% block object-tools-items %}
|
|
33
|
+
{{ block.super }}
|
|
34
|
+
<span class="block bg-base-200 h-5 w-px dark:bg-base-700 import-export-separator"></span>
|
|
35
|
+
<div class="import-export-group">
|
|
36
|
+
{% include "admin/import_export/change_list_import_item.html" %}
|
|
37
|
+
{% include "admin/import_export/change_list_export_item.html" %}
|
|
38
|
+
</div>
|
|
39
|
+
{% endblock %}
|
django_cfg/modules/django_import_export/templates/admin/import_export/change_list_import_item.html
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{% load admin_urls i18n %}
|
|
2
|
+
|
|
3
|
+
{% if has_import_permission %}
|
|
4
|
+
<a href="import/" class="bg-green-600 flex items-center h-9 justify-center -my-1 rounded-full w-9 hover:bg-green-700" title="{% trans 'Import' %}">
|
|
5
|
+
<span class="material-symbols-outlined text-white">upload</span>
|
|
6
|
+
</a>
|
|
7
|
+
{% endif %}
|
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.106"
|
|
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",]
|
|
@@ -180,6 +180,63 @@ resetIframe(tab) {
|
|
|
180
180
|
**Why Reset?**
|
|
181
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
182
|
|
|
183
|
+
### Open in New Window
|
|
184
|
+
|
|
185
|
+
The External Admin tab (Tab 2) includes an "Open in New Window" button that allows users to break out of the iframe and work in a dedicated browser window/tab:
|
|
186
|
+
|
|
187
|
+
```javascript
|
|
188
|
+
openInNewWindow() {
|
|
189
|
+
// Get the current iframe URL for the External Admin tab
|
|
190
|
+
const iframe = document.getElementById('nextjs-dashboard-iframe-nextjs');
|
|
191
|
+
if (iframe) {
|
|
192
|
+
const currentUrl = iframe.src || iframe.getAttribute('data-original-src');
|
|
193
|
+
if (currentUrl) {
|
|
194
|
+
window.open(currentUrl, '_blank', 'noopener,noreferrer');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Features:**
|
|
201
|
+
- Only visible when External Admin tab is active (`x-show="activeTab === 'nextjs'"`)
|
|
202
|
+
- Opens current iframe URL in new window with `noopener,noreferrer` security flags
|
|
203
|
+
- Preserves current route via `postMessage` tracking (see below)
|
|
204
|
+
- Styled as action button with icon and text label
|
|
205
|
+
|
|
206
|
+
**How Route Tracking Works:**
|
|
207
|
+
|
|
208
|
+
In **production** (same-origin), `iframe.src` updates automatically:
|
|
209
|
+
```javascript
|
|
210
|
+
// iframe.src reflects current URL automatically
|
|
211
|
+
window.open(iframe.src, '_blank');
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
In **development** (cross-origin), we track navigation via `postMessage`:
|
|
215
|
+
```javascript
|
|
216
|
+
// iframe sends navigation events
|
|
217
|
+
case 'iframe-navigation':
|
|
218
|
+
if (data?.path) {
|
|
219
|
+
alpineData.currentNextjsPath = data.path; // Track path
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Button uses tracked path
|
|
223
|
+
openInNewWindow() {
|
|
224
|
+
const url = new URL(baseUrl);
|
|
225
|
+
url.pathname = this.currentNextjsPath; // Apply tracked path
|
|
226
|
+
window.open(url.toString(), '_blank');
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**Why postMessage?**
|
|
231
|
+
Cross-origin iframes cannot access `iframe.src` due to browser security (CORS). The iframe must explicitly send navigation events via `postMessage`.
|
|
232
|
+
|
|
233
|
+
**Why This is Useful:**
|
|
234
|
+
- Full browser features (address bar, bookmarks, etc.)
|
|
235
|
+
- No iframe sandbox restrictions
|
|
236
|
+
- Easier debugging (browser DevTools)
|
|
237
|
+
- Better for complex workflows that require multiple windows
|
|
238
|
+
- Copy/paste and other browser features work better
|
|
239
|
+
|
|
183
240
|
## Static File Serving
|
|
184
241
|
|
|
185
242
|
### Built-in Admin (Tab 1)
|
|
@@ -136,6 +136,7 @@
|
|
|
136
136
|
<div x-data="{
|
|
137
137
|
activeTab: 'builtin',
|
|
138
138
|
previousTab: 'builtin',
|
|
139
|
+
currentNextjsPath: '',
|
|
139
140
|
switchTab(tab) {
|
|
140
141
|
if (this.previousTab !== tab) {
|
|
141
142
|
// Reset iframe to initial URL when switching tabs
|
|
@@ -152,6 +153,34 @@
|
|
|
152
153
|
const originalSrc = iframe.getAttribute('data-original-src') || iframe.src;
|
|
153
154
|
iframe.src = originalSrc;
|
|
154
155
|
}
|
|
156
|
+
// Reset path when resetting iframe
|
|
157
|
+
if (tab === 'nextjs') {
|
|
158
|
+
this.currentNextjsPath = '';
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
openInNewWindow() {
|
|
162
|
+
// Get the current iframe URL for the External Admin tab
|
|
163
|
+
const iframe = document.getElementById('nextjs-dashboard-iframe-nextjs');
|
|
164
|
+
if (!iframe) return;
|
|
165
|
+
|
|
166
|
+
// Get base URL from iframe src or data-original-src
|
|
167
|
+
let baseUrl = iframe.src || iframe.getAttribute('data-original-src');
|
|
168
|
+
|
|
169
|
+
// If we have a tracked path from postMessage, use it
|
|
170
|
+
if (this.currentNextjsPath) {
|
|
171
|
+
try {
|
|
172
|
+
const url = new URL(baseUrl);
|
|
173
|
+
// Replace pathname with tracked path
|
|
174
|
+
url.pathname = this.currentNextjsPath;
|
|
175
|
+
baseUrl = url.toString();
|
|
176
|
+
} catch (e) {
|
|
177
|
+
console.warn('[Django-CFG] Failed to construct URL with path:', e);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (baseUrl) {
|
|
182
|
+
window.open(baseUrl, '_blank', 'noopener,noreferrer');
|
|
183
|
+
}
|
|
155
184
|
}
|
|
156
185
|
}">
|
|
157
186
|
{% if is_frontend_dev_mode %}
|
|
@@ -200,12 +229,25 @@
|
|
|
200
229
|
</button>
|
|
201
230
|
</nav>
|
|
202
231
|
|
|
203
|
-
<!-- Version info -->
|
|
204
|
-
<div class="
|
|
205
|
-
|
|
206
|
-
<
|
|
207
|
-
|
|
208
|
-
|
|
232
|
+
<!-- Actions & Version info -->
|
|
233
|
+
<div class="flex items-center gap-4 py-4">
|
|
234
|
+
<!-- Open in new window button (only for External Admin tab) -->
|
|
235
|
+
<button @click="openInNewWindow()"
|
|
236
|
+
x-show="activeTab === 'nextjs'"
|
|
237
|
+
title="Open in new window"
|
|
238
|
+
class="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md text-gray-600 hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-gray-100 dark:hover:bg-gray-700 transition-all duration-150"
|
|
239
|
+
style="display: none;">
|
|
240
|
+
<span class="material-icons" style="font-size: 16px;">open_in_new</span>
|
|
241
|
+
<span>Open in New Window</span>
|
|
242
|
+
</button>
|
|
243
|
+
|
|
244
|
+
<!-- Version info -->
|
|
245
|
+
<div class="text-xs text-gray-400 dark:text-gray-500">
|
|
246
|
+
{% load django_cfg %}
|
|
247
|
+
<a href="{% lib_site_url %}" class="text-blue-600 hover:text-blue-700">
|
|
248
|
+
{% lib_name %}
|
|
249
|
+
</a>
|
|
250
|
+
</div>
|
|
209
251
|
</div>
|
|
210
252
|
</div>
|
|
211
253
|
</div>
|
|
@@ -419,7 +461,18 @@
|
|
|
419
461
|
break;
|
|
420
462
|
|
|
421
463
|
case 'iframe-navigation':
|
|
422
|
-
|
|
464
|
+
console.log('[Django-CFG] iframe navigated to:', data?.path);
|
|
465
|
+
// Track current path for "Open in New Window" button
|
|
466
|
+
if (iframe.id === 'nextjs-dashboard-iframe-nextjs' && data?.path) {
|
|
467
|
+
// Update Alpine.js data
|
|
468
|
+
const alpineEl = document.querySelector('[x-data]');
|
|
469
|
+
if (alpineEl && window.Alpine) {
|
|
470
|
+
const alpineData = window.Alpine.$data(alpineEl);
|
|
471
|
+
if (alpineData) {
|
|
472
|
+
alpineData.currentNextjsPath = data.path;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
423
476
|
break;
|
|
424
477
|
|
|
425
478
|
default:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-cfg
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.106
|
|
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=a9PJ3jnY_fzqDDHW2WT6IkYi7nMmZ7BbcBqBL7AtKfY,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
|
|
@@ -577,7 +577,7 @@ django_cfg/core/generation/protocols.py,sha256=9pqGsZD_Y5v3jdYp4YdkL-i--8feuvX_i
|
|
|
577
577
|
django_cfg/core/generation/core_generators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
578
578
|
django_cfg/core/generation/core_generators/settings.py,sha256=Rto9qjqQC-MeQJMNyoflS91RrNDS_lBNH4d4ZtgER-8,2632
|
|
579
579
|
django_cfg/core/generation/core_generators/static.py,sha256=7xo7F9DSbz5Gk2Di-SbBo0petNMX4ZKfS8wuQCe9lz8,2747
|
|
580
|
-
django_cfg/core/generation/core_generators/templates.py,sha256=
|
|
580
|
+
django_cfg/core/generation/core_generators/templates.py,sha256=XeGKzF790_zKwFdRw3FN-VHCEgw6Q1dWPX7C1tNas1s,4885
|
|
581
581
|
django_cfg/core/generation/data_generators/__init__.py,sha256=O7cmt_k34hWRgrdaAzCRPudcaowaOwMgaoXmZY8HG54,298
|
|
582
582
|
django_cfg/core/generation/data_generators/cache.py,sha256=bdcbnaDarl_8UQ18gk7bIKrQ9PDZOhpDAVjCEgYVeUU,3840
|
|
583
583
|
django_cfg/core/generation/data_generators/database.py,sha256=mYR2mBvqWU93YP9XUUVlcZaQJtSNeAQ7Dk4Vj6Tu95k,3481
|
|
@@ -695,9 +695,10 @@ django_cfg/models/tasks/utils.py,sha256=5CR_d6dD6vYZ9UpI3kKD9EJwuPdWWuqiA_hhxXzm
|
|
|
695
695
|
django_cfg/modules/__init__.py,sha256=Ip9WMpzImEwIAywpFwU056_v0O9oIGG7nCT1YSArxkw,316
|
|
696
696
|
django_cfg/modules/base.py,sha256=Grmgxc5dvnAEM1sudWEWO4kv8L0Ks-y32nxTk2vwdjQ,6272
|
|
697
697
|
django_cfg/modules/django_admin/CHANGELOG_CODE_METHODS.md,sha256=HfE_rUlovx1zX_1hkzQsjwghaFvIvUWjZ_Aume8lhIs,3823
|
|
698
|
+
django_cfg/modules/django_admin/IMPORT_EXPORT_FIX.md,sha256=F8z4oHZk7jkRChxW8tKsVf0Q_OujjlBUs8UJNirn55Y,2055
|
|
698
699
|
django_cfg/modules/django_admin/__init__.py,sha256=rWUY6Le2gO-szuuQyrUUP8sLIaTwkNDBexdK8Vbwzv0,3094
|
|
699
700
|
django_cfg/modules/django_admin/base/__init__.py,sha256=tzre09bnD_SlS-pA30WzYZRxyvch7eLq3q0wLEcZOmc,118
|
|
700
|
-
django_cfg/modules/django_admin/base/pydantic_admin.py,sha256=
|
|
701
|
+
django_cfg/modules/django_admin/base/pydantic_admin.py,sha256=srGVXDdlb0YIcP_VIpVFjLegIkg7vEKp8GlAidtoBc0,18639
|
|
701
702
|
django_cfg/modules/django_admin/base/unfold_admin.py,sha256=iqpRWSkzW5HktXDuuG7G3J6RoIfW48dWPMJTa7Yk08g,729
|
|
702
703
|
django_cfg/modules/django_admin/config/__init__.py,sha256=UJGJMP1iAguzd33E1BgeIjWaooFYylku3aR_Arib-cg,604
|
|
703
704
|
django_cfg/modules/django_admin/config/action_config.py,sha256=JjS01JxLT-FzUVq7RlKaB7L38wmVL8uibXO_iXZcljo,1668
|
|
@@ -909,8 +910,14 @@ django_cfg/modules/django_email/management/commands/__init__.py,sha256=47DEQpj8H
|
|
|
909
910
|
django_cfg/modules/django_email/management/commands/test_email.py,sha256=PecBUXIkT0tlH6_ZbhLOrju_lKCnsfICuFlWBpXIyGk,3316
|
|
910
911
|
django_cfg/modules/django_health/__init__.py,sha256=lX8uJlAmXZeAVfQy12H67uTTy5SOjYMu3N8d8WF8ZNM,156
|
|
911
912
|
django_cfg/modules/django_health/service.py,sha256=K7Fm3fmRjrjRiPpaQIfyaQ4cI3coSIX2HrsRXjUzxOk,8837
|
|
912
|
-
django_cfg/modules/django_import_export/README.md,sha256=
|
|
913
|
-
django_cfg/modules/django_import_export/__init__.py,sha256=
|
|
913
|
+
django_cfg/modules/django_import_export/README.md,sha256=Oha18L7skITg-5oOfcKEvwealrD7FIziXt2y66WoaYM,3115
|
|
914
|
+
django_cfg/modules/django_import_export/__init__.py,sha256=vnMEpayU1RCmsC3yC38oIRvZzlmcLg0d0Y24fzQbl0Y,2418
|
|
915
|
+
django_cfg/modules/django_import_export/apps.py,sha256=W_yhcefsNxz8aYFCwacFmeFeTZ0clUTGU0gL-GfGOQ4,560
|
|
916
|
+
django_cfg/modules/django_import_export/templates/admin/import_export/change_list_export.html,sha256=RcbmcksrwUtZVA_hKszu2pMOPw3I758qsNWMNJSrDJk,177
|
|
917
|
+
django_cfg/modules/django_import_export/templates/admin/import_export/change_list_export_item.html,sha256=Lq_c0KS-FEbG7eCo6XjUSgB-Mp9XOr_m7Mn77Z2f_CA,330
|
|
918
|
+
django_cfg/modules/django_import_export/templates/admin/import_export/change_list_import.html,sha256=XW89hAnilxQT2TKuIpQ4SPtxcdnRMl_1X_h_Ie5V2Xc,177
|
|
919
|
+
django_cfg/modules/django_import_export/templates/admin/import_export/change_list_import_export.html,sha256=BHr4h4aX7_ZzodfwisVbAewf8qKOuhGNeseURcMmK7o,1269
|
|
920
|
+
django_cfg/modules/django_import_export/templates/admin/import_export/change_list_import_item.html,sha256=XYsTEbQc3CCQR06-4qazXx-svQ0RoEDE6YPKoZw1oKo,305
|
|
914
921
|
django_cfg/modules/django_llm/README.md,sha256=31uQG9_z-HroEUevtOPJbZ86H9yiwzZJs0omf-Kwh-o,9374
|
|
915
922
|
django_cfg/modules/django_llm/__init__.py,sha256=tksyeH6e5ajrSNa6MVkeu_IZQE_I-zka1iTLAIU-WtE,1913
|
|
916
923
|
django_cfg/modules/django_llm/example.py,sha256=PqOZXV1qY8GjFfRdOM3a8Rjj59CzDbETokkvPqAkkm8,12797
|
|
@@ -1074,8 +1081,8 @@ django_cfg/static/js/api/support/index.mjs,sha256=oPA3iGkUWYyKQuJlI5-tSxD3AOhwlA
|
|
|
1074
1081
|
django_cfg/static/js/api/tasks/client.mjs,sha256=tIy8K-finXzTUL9kOo_L4Q1kchDaHyuzjwS4VymiWPM,3579
|
|
1075
1082
|
django_cfg/static/js/api/tasks/index.mjs,sha256=yCY1GzdD-RtFZ3pAfk1l0msgO1epyo0lsGCjH0g1Afc,294
|
|
1076
1083
|
django_cfg/templates/__init__.py,sha256=IzLjt-a7VIJ0OutmAE1_-w0_LpL2u0MgGpnIabjZuW8,19
|
|
1077
|
-
django_cfg/templates/admin/DUAL_TAB_ARCHITECTURE.md,sha256=
|
|
1078
|
-
django_cfg/templates/admin/index.html,sha256=
|
|
1084
|
+
django_cfg/templates/admin/DUAL_TAB_ARCHITECTURE.md,sha256=CL8E3K4rFpXeQiNgrYSMvCW1y-eFaoXxxsI58zPf9dY,17562
|
|
1085
|
+
django_cfg/templates/admin/index.html,sha256=T8atxHnk7f6hiRDA4SKhpRY76srDB-auLANmjSSbejs,20654
|
|
1079
1086
|
django_cfg/templates/emails/base_email.html,sha256=TWcvYa2IHShlF_E8jf1bWZStRO0v8G4L_GexPxvz6XQ,8836
|
|
1080
1087
|
django_cfg/templates/unfold/layouts/skeleton.html,sha256=2ArkcNZ34mFs30cOAsTQ1EZiDXcB0aVxkO71lJq9SLE,718
|
|
1081
1088
|
django_cfg/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -1087,9 +1094,9 @@ django_cfg/utils/version_check.py,sha256=WO51J2m2e-wVqWCRwbultEwu3q1lQasV67Mw2aa
|
|
|
1087
1094
|
django_cfg/CHANGELOG.md,sha256=jtT3EprqEJkqSUh7IraP73vQ8PmKUMdRtznQsEnqDZk,2052
|
|
1088
1095
|
django_cfg/CONTRIBUTING.md,sha256=DU2kyQ6PU0Z24ob7O_OqKWEYHcZmJDgzw-lQCmu6uBg,3041
|
|
1089
1096
|
django_cfg/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
|
|
1090
|
-
django_cfg/pyproject.toml,sha256=
|
|
1091
|
-
django_cfg-1.4.
|
|
1092
|
-
django_cfg-1.4.
|
|
1093
|
-
django_cfg-1.4.
|
|
1094
|
-
django_cfg-1.4.
|
|
1095
|
-
django_cfg-1.4.
|
|
1097
|
+
django_cfg/pyproject.toml,sha256=Azsm48lHmm_ksNBsj-MZGKqdiLerHiVMQ71jY8MldP8,8573
|
|
1098
|
+
django_cfg-1.4.106.dist-info/METADATA,sha256=Dl1J5WrtjhAyxfHAWz64bAnAhjZ8JXz9djjDwg2-epg,23734
|
|
1099
|
+
django_cfg-1.4.106.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
1100
|
+
django_cfg-1.4.106.dist-info/entry_points.txt,sha256=Ucmde4Z2wEzgb4AggxxZ0zaYDb9HpyE5blM3uJ0_VNg,56
|
|
1101
|
+
django_cfg-1.4.106.dist-info/licenses/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
|
|
1102
|
+
django_cfg-1.4.106.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|