django-cfg 1.4.105__py3-none-any.whl → 1.4.107__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/RESOURCE_CONFIG_ENHANCEMENT.md +350 -0
- django_cfg/modules/django_admin/__init__.py +4 -0
- django_cfg/modules/django_admin/base/pydantic_admin.py +126 -7
- django_cfg/modules/django_admin/config/__init__.py +4 -0
- django_cfg/modules/django_admin/config/admin_config.py +13 -1
- django_cfg/modules/django_admin/config/background_task_config.py +76 -0
- django_cfg/modules/django_admin/config/resource_config.py +129 -0
- 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 +2 -2
- {django_cfg-1.4.105.dist-info → django_cfg-1.4.107.dist-info}/METADATA +3 -1
- {django_cfg-1.4.105.dist-info → django_cfg-1.4.107.dist-info}/RECORD +24 -14
- {django_cfg-1.4.105.dist-info → django_cfg-1.4.107.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.105.dist-info → django_cfg-1.4.107.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.105.dist-info → django_cfg-1.4.107.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!
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
# ResourceConfig & BackgroundTaskConfig Enhancement
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
Enhanced django-admin module with declarative import/export configuration and background task support.
|
|
6
|
+
|
|
7
|
+
## New Features
|
|
8
|
+
|
|
9
|
+
### 1. ResourceConfig
|
|
10
|
+
|
|
11
|
+
Declarative configuration for django-import-export Resource classes.
|
|
12
|
+
|
|
13
|
+
**Location:** `django_cfg/modules/django_admin/config/resource_config.py`
|
|
14
|
+
|
|
15
|
+
**Features:**
|
|
16
|
+
- Field selection and exclusion
|
|
17
|
+
- Import ID fields configuration
|
|
18
|
+
- Import/export behavior control
|
|
19
|
+
- Hook support (before/after import, per-row hooks)
|
|
20
|
+
- Export field ordering
|
|
21
|
+
- Batch processing options
|
|
22
|
+
|
|
23
|
+
**Example:**
|
|
24
|
+
```python
|
|
25
|
+
from django_cfg.modules.django_admin import ResourceConfig
|
|
26
|
+
|
|
27
|
+
resource_config = ResourceConfig(
|
|
28
|
+
fields=['host', 'port', 'username', 'password'],
|
|
29
|
+
exclude=['metadata', 'config'],
|
|
30
|
+
import_id_fields=['host', 'port'],
|
|
31
|
+
skip_unchanged=True,
|
|
32
|
+
after_import_row='apps.myapp.tasks.test_after_import',
|
|
33
|
+
batch_size=100,
|
|
34
|
+
)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2. BackgroundTaskConfig
|
|
38
|
+
|
|
39
|
+
Configuration for background task processing.
|
|
40
|
+
|
|
41
|
+
**Location:** `django_cfg/modules/django_admin/config/background_task_config.py`
|
|
42
|
+
|
|
43
|
+
**Features:**
|
|
44
|
+
- Task runner selection (rearq, celery, django_q, sync)
|
|
45
|
+
- Batch size configuration
|
|
46
|
+
- Timeout settings
|
|
47
|
+
- Retry policy
|
|
48
|
+
- Priority levels
|
|
49
|
+
|
|
50
|
+
**Example:**
|
|
51
|
+
```python
|
|
52
|
+
from django_cfg.modules.django_admin import BackgroundTaskConfig
|
|
53
|
+
|
|
54
|
+
background_config = BackgroundTaskConfig(
|
|
55
|
+
enabled=True,
|
|
56
|
+
task_runner='rearq',
|
|
57
|
+
batch_size=50,
|
|
58
|
+
timeout=300,
|
|
59
|
+
retry_on_failure=True,
|
|
60
|
+
max_retries=3,
|
|
61
|
+
)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 3. Enhanced AdminConfig
|
|
65
|
+
|
|
66
|
+
**Updated:** `django_cfg/modules/django_admin/config/admin_config.py`
|
|
67
|
+
|
|
68
|
+
**New Fields:**
|
|
69
|
+
```python
|
|
70
|
+
class AdminConfig(BaseModel):
|
|
71
|
+
# ... existing fields ...
|
|
72
|
+
|
|
73
|
+
# Import/Export enhancement
|
|
74
|
+
resource_config: Optional[ResourceConfig] = None
|
|
75
|
+
|
|
76
|
+
# Background task processing
|
|
77
|
+
background_task_config: Optional[BackgroundTaskConfig] = None
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 4. Enhanced PydanticAdmin
|
|
81
|
+
|
|
82
|
+
**Updated:** `django_cfg/modules/django_admin/base/pydantic_admin.py`
|
|
83
|
+
|
|
84
|
+
**Key Changes:**
|
|
85
|
+
- `_generate_resource_class()` now uses ResourceConfig if provided
|
|
86
|
+
- Auto-generates Resource with hooks support
|
|
87
|
+
- Dynamically attaches hook methods to Resource class
|
|
88
|
+
|
|
89
|
+
**Hook Support:**
|
|
90
|
+
- `before_import` - Called before import starts
|
|
91
|
+
- `after_import` - Called after import completes
|
|
92
|
+
- `before_import_row` - Called before each row import
|
|
93
|
+
- `after_import_row` - Called after each row import (★ most useful)
|
|
94
|
+
|
|
95
|
+
## Usage Example
|
|
96
|
+
|
|
97
|
+
### Complete Proxy Admin with Import/Export
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from django_cfg.modules.django_admin import (
|
|
101
|
+
ActionConfig,
|
|
102
|
+
AdminConfig,
|
|
103
|
+
BackgroundTaskConfig,
|
|
104
|
+
ResourceConfig,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
proxy_config = AdminConfig(
|
|
108
|
+
model=Proxy,
|
|
109
|
+
|
|
110
|
+
# Enable import/export with ResourceConfig
|
|
111
|
+
import_export_enabled=True,
|
|
112
|
+
resource_config=ResourceConfig(
|
|
113
|
+
fields=[
|
|
114
|
+
'host', 'port', 'proxy_type', 'proxy_mode',
|
|
115
|
+
'username', 'password',
|
|
116
|
+
'provider', 'country',
|
|
117
|
+
],
|
|
118
|
+
exclude=['metadata', 'config', 'last_error'],
|
|
119
|
+
import_id_fields=['host', 'port', 'provider'],
|
|
120
|
+
skip_unchanged=True,
|
|
121
|
+
# Auto-test after import
|
|
122
|
+
after_import_row='apps.proxies.tasks.after_import_row_test_proxy',
|
|
123
|
+
),
|
|
124
|
+
|
|
125
|
+
# Background task configuration
|
|
126
|
+
background_task_config=BackgroundTaskConfig(
|
|
127
|
+
enabled=True,
|
|
128
|
+
task_runner='rearq',
|
|
129
|
+
batch_size=50,
|
|
130
|
+
timeout=300,
|
|
131
|
+
),
|
|
132
|
+
|
|
133
|
+
# Admin actions
|
|
134
|
+
actions=[
|
|
135
|
+
ActionConfig(
|
|
136
|
+
name='test_selected_proxies',
|
|
137
|
+
description='Test selected proxies',
|
|
138
|
+
variant='warning',
|
|
139
|
+
icon='speed',
|
|
140
|
+
handler='apps.proxies.admin.actions.test_selected_proxies',
|
|
141
|
+
),
|
|
142
|
+
],
|
|
143
|
+
|
|
144
|
+
list_display=['host', 'port', 'status', 'success_rate'],
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
@admin.register(Proxy)
|
|
148
|
+
class ProxyAdmin(PydanticAdmin):
|
|
149
|
+
config = proxy_config
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Hook Implementation
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
# apps/proxies/tasks.py
|
|
156
|
+
|
|
157
|
+
def after_import_row_test_proxy(row, row_result, **kwargs):
|
|
158
|
+
"""Hook called after each proxy import."""
|
|
159
|
+
|
|
160
|
+
# Skip if dry run
|
|
161
|
+
if kwargs.get('dry_run'):
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
# Only test new proxies
|
|
165
|
+
if row_result.import_type == 'new':
|
|
166
|
+
proxy = row_result.instance
|
|
167
|
+
|
|
168
|
+
# Queue async test
|
|
169
|
+
from api.workers import get_worker
|
|
170
|
+
worker = get_worker()
|
|
171
|
+
worker.enqueue_task(
|
|
172
|
+
'apps.proxies.tasks.test_proxy_async',
|
|
173
|
+
proxy_id=str(proxy.id)
|
|
174
|
+
)
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Benefits
|
|
178
|
+
|
|
179
|
+
### Before (Manual Resource Class)
|
|
180
|
+
```python
|
|
181
|
+
from import_export import resources
|
|
182
|
+
|
|
183
|
+
class ProxyResource(resources.ModelResource):
|
|
184
|
+
class Meta:
|
|
185
|
+
model = Proxy
|
|
186
|
+
fields = ('host', 'port', 'username', 'password')
|
|
187
|
+
import_id_fields = ['host', 'port']
|
|
188
|
+
skip_unchanged = True
|
|
189
|
+
|
|
190
|
+
def after_import_row(self, row, row_result, **kwargs):
|
|
191
|
+
# Custom logic here
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
proxy_config = AdminConfig(
|
|
195
|
+
model=Proxy,
|
|
196
|
+
import_export_enabled=True,
|
|
197
|
+
resource_class=ProxyResource, # Manual class
|
|
198
|
+
)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### After (Declarative ResourceConfig)
|
|
202
|
+
```python
|
|
203
|
+
proxy_config = AdminConfig(
|
|
204
|
+
model=Proxy,
|
|
205
|
+
import_export_enabled=True,
|
|
206
|
+
resource_config=ResourceConfig(
|
|
207
|
+
fields=['host', 'port', 'username', 'password'],
|
|
208
|
+
import_id_fields=['host', 'port'],
|
|
209
|
+
skip_unchanged=True,
|
|
210
|
+
after_import_row='apps.proxies.tasks.after_import_row_test_proxy',
|
|
211
|
+
),
|
|
212
|
+
)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**Advantages:**
|
|
216
|
+
- ✅ No separate Resource class needed
|
|
217
|
+
- ✅ All configuration in one place
|
|
218
|
+
- ✅ Type-safe with Pydantic validation
|
|
219
|
+
- ✅ Reusable across multiple admins
|
|
220
|
+
- ✅ Hook as string path (lazy import)
|
|
221
|
+
- ✅ Auto-generated Resource with full control
|
|
222
|
+
|
|
223
|
+
## Dependencies Added
|
|
224
|
+
|
|
225
|
+
**pyproject.toml updates:**
|
|
226
|
+
```toml
|
|
227
|
+
dependencies = [
|
|
228
|
+
# ... existing ...
|
|
229
|
+
"pytz>=2025.1",
|
|
230
|
+
"httpx>=0.28.1,<1.0",
|
|
231
|
+
]
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
- `pytz` - Timezone support for datetime operations
|
|
235
|
+
- `httpx` - Modern HTTP client for proxy testing
|
|
236
|
+
|
|
237
|
+
## Files Changed
|
|
238
|
+
|
|
239
|
+
### django-cfg Core
|
|
240
|
+
1. `config/resource_config.py` - NEW
|
|
241
|
+
2. `config/background_task_config.py` - NEW
|
|
242
|
+
3. `config/admin_config.py` - MODIFIED (added new configs)
|
|
243
|
+
4. `config/__init__.py` - MODIFIED (exports)
|
|
244
|
+
5. `base/pydantic_admin.py` - MODIFIED (Resource generation)
|
|
245
|
+
6. `__init__.py` - MODIFIED (exports)
|
|
246
|
+
7. `pyproject.toml` - MODIFIED (dependencies)
|
|
247
|
+
|
|
248
|
+
### stockapis Implementation
|
|
249
|
+
1. `apps/proxies/admin/proxy_admin.py` - Uses ResourceConfig
|
|
250
|
+
2. `apps/proxies/admin/actions.py` - NEW (admin actions)
|
|
251
|
+
3. `apps/proxies/services/proxy_tester.py` - NEW (testing logic)
|
|
252
|
+
4. `apps/proxies/tasks.py` - NEW (background tasks)
|
|
253
|
+
5. `apps/proxies/@docs/IMPORT_EXPORT_SETUP.md` - NEW (docs)
|
|
254
|
+
|
|
255
|
+
## Backward Compatibility
|
|
256
|
+
|
|
257
|
+
✅ **100% Backward Compatible**
|
|
258
|
+
|
|
259
|
+
Old code still works:
|
|
260
|
+
```python
|
|
261
|
+
# Old way - still works
|
|
262
|
+
proxy_config = AdminConfig(
|
|
263
|
+
model=Proxy,
|
|
264
|
+
import_export_enabled=True,
|
|
265
|
+
resource_class=MyCustomResource, # Manual class
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# New way - alternative
|
|
269
|
+
proxy_config = AdminConfig(
|
|
270
|
+
model=Proxy,
|
|
271
|
+
import_export_enabled=True,
|
|
272
|
+
resource_config=ResourceConfig(...), # Declarative
|
|
273
|
+
)
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Priority:
|
|
277
|
+
1. If `resource_class` provided → use it (legacy)
|
|
278
|
+
2. If `resource_config` provided → auto-generate Resource
|
|
279
|
+
3. If neither → auto-generate basic Resource
|
|
280
|
+
|
|
281
|
+
## Testing
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
# Test django-cfg imports
|
|
285
|
+
cd django-cfg-dev
|
|
286
|
+
poetry run python -c "from django_cfg.modules.django_admin import ResourceConfig, BackgroundTaskConfig; print('✅ OK')"
|
|
287
|
+
|
|
288
|
+
# Test stockapis admin
|
|
289
|
+
cd stockapis
|
|
290
|
+
poetry run python manage.py check --tag admin
|
|
291
|
+
|
|
292
|
+
# Test in browser
|
|
293
|
+
poetry run python manage.py runserver
|
|
294
|
+
# Visit: http://localhost:8000/admin/proxies/proxy/
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Future Enhancements
|
|
298
|
+
|
|
299
|
+
- [ ] `ResourceConfig.field_widgets` - Custom widgets for fields
|
|
300
|
+
- [ ] `ResourceConfig.validators` - Custom validation rules
|
|
301
|
+
- [ ] `ResourceConfig.transformers` - Data transformation before import
|
|
302
|
+
- [ ] Progress tracking for large imports
|
|
303
|
+
- [ ] Import history and rollback
|
|
304
|
+
- [ ] Scheduled imports via cron
|
|
305
|
+
- [ ] API endpoint for programmatic imports
|
|
306
|
+
|
|
307
|
+
## Migration Guide
|
|
308
|
+
|
|
309
|
+
### For Existing Projects
|
|
310
|
+
|
|
311
|
+
1. Update django-cfg to latest version
|
|
312
|
+
2. Optional: Replace manual Resource classes with ResourceConfig
|
|
313
|
+
3. Optional: Add BackgroundTaskConfig for async operations
|
|
314
|
+
4. Optional: Add after_import_row hooks for post-import processing
|
|
315
|
+
|
|
316
|
+
### Example Migration
|
|
317
|
+
|
|
318
|
+
**Before:**
|
|
319
|
+
```python
|
|
320
|
+
# resources.py
|
|
321
|
+
class ProxyResource(resources.ModelResource):
|
|
322
|
+
class Meta:
|
|
323
|
+
model = Proxy
|
|
324
|
+
fields = ('host', 'port')
|
|
325
|
+
|
|
326
|
+
# admin.py
|
|
327
|
+
config = AdminConfig(
|
|
328
|
+
model=Proxy,
|
|
329
|
+
import_export_enabled=True,
|
|
330
|
+
resource_class=ProxyResource,
|
|
331
|
+
)
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
**After:**
|
|
335
|
+
```python
|
|
336
|
+
# admin.py only - no resources.py needed
|
|
337
|
+
config = AdminConfig(
|
|
338
|
+
model=Proxy,
|
|
339
|
+
import_export_enabled=True,
|
|
340
|
+
resource_config=ResourceConfig(
|
|
341
|
+
fields=['host', 'port'],
|
|
342
|
+
),
|
|
343
|
+
)
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
## Version
|
|
347
|
+
|
|
348
|
+
- **django-cfg**: 1.4.106+
|
|
349
|
+
- **Python**: 3.12+
|
|
350
|
+
- **Django**: 5.2+
|
|
@@ -46,6 +46,7 @@ Example:
|
|
|
46
46
|
from .config import (
|
|
47
47
|
ActionConfig,
|
|
48
48
|
AdminConfig,
|
|
49
|
+
BackgroundTaskConfig,
|
|
49
50
|
BadgeField,
|
|
50
51
|
BooleanField,
|
|
51
52
|
CurrencyField,
|
|
@@ -53,6 +54,7 @@ from .config import (
|
|
|
53
54
|
FieldConfig,
|
|
54
55
|
FieldsetConfig,
|
|
55
56
|
ImageField,
|
|
57
|
+
ResourceConfig,
|
|
56
58
|
TextField,
|
|
57
59
|
UserField,
|
|
58
60
|
)
|
|
@@ -101,6 +103,8 @@ __all__ = [
|
|
|
101
103
|
"FieldConfig",
|
|
102
104
|
"FieldsetConfig",
|
|
103
105
|
"ActionConfig",
|
|
106
|
+
"ResourceConfig",
|
|
107
|
+
"BackgroundTaskConfig",
|
|
104
108
|
# Specialized Field Types
|
|
105
109
|
"BadgeField",
|
|
106
110
|
"BooleanField",
|
|
@@ -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,120 @@ 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
|
+
"""
|
|
158
|
+
Auto-generate a ModelResource class for import/export.
|
|
159
|
+
|
|
160
|
+
Uses ResourceConfig if provided, otherwise generates basic Resource.
|
|
161
|
+
"""
|
|
162
|
+
from import_export import resources
|
|
163
|
+
|
|
164
|
+
target_model = config.model
|
|
165
|
+
resource_config = config.resource_config
|
|
166
|
+
|
|
167
|
+
# Determine fields to include
|
|
168
|
+
if resource_config and resource_config.fields:
|
|
169
|
+
# Use explicitly specified fields
|
|
170
|
+
model_fields = resource_config.fields
|
|
171
|
+
else:
|
|
172
|
+
# Auto-detect fields from model
|
|
173
|
+
model_fields = []
|
|
174
|
+
for field in target_model._meta.get_fields():
|
|
175
|
+
# Skip relations and auto fields that shouldn't be imported
|
|
176
|
+
if field.concrete and not field.many_to_many:
|
|
177
|
+
# Skip password fields for security
|
|
178
|
+
if 'password' not in field.name.lower():
|
|
179
|
+
model_fields.append(field.name)
|
|
180
|
+
|
|
181
|
+
# Apply exclusions if specified
|
|
182
|
+
if resource_config and resource_config.exclude:
|
|
183
|
+
model_fields = [f for f in model_fields if f not in resource_config.exclude]
|
|
184
|
+
|
|
185
|
+
# Build Meta attributes
|
|
186
|
+
meta_attrs = {
|
|
187
|
+
'model': target_model,
|
|
188
|
+
'fields': tuple(model_fields),
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
# Add ResourceConfig settings
|
|
192
|
+
if resource_config:
|
|
193
|
+
meta_attrs['import_id_fields'] = resource_config.import_id_fields
|
|
194
|
+
meta_attrs['skip_unchanged'] = resource_config.skip_unchanged
|
|
195
|
+
meta_attrs['report_skipped'] = resource_config.report_skipped
|
|
196
|
+
meta_attrs['use_transactions'] = resource_config.use_transactions
|
|
197
|
+
|
|
198
|
+
if resource_config.export_order:
|
|
199
|
+
meta_attrs['export_order'] = tuple(resource_config.export_order)
|
|
200
|
+
else:
|
|
201
|
+
# Default settings
|
|
202
|
+
meta_attrs['import_id_fields'] = ['id'] if 'id' in model_fields else []
|
|
203
|
+
meta_attrs['skip_unchanged'] = True
|
|
204
|
+
meta_attrs['report_skipped'] = True
|
|
205
|
+
|
|
206
|
+
# Create Meta class
|
|
207
|
+
ResourceMeta = type('Meta', (), meta_attrs)
|
|
208
|
+
|
|
209
|
+
# Build Resource class attributes (methods + Meta)
|
|
210
|
+
resource_attrs = {'Meta': ResourceMeta}
|
|
211
|
+
|
|
212
|
+
# Add hooks from ResourceConfig
|
|
213
|
+
if resource_config:
|
|
214
|
+
# before_import hook
|
|
215
|
+
if resource_config.before_import:
|
|
216
|
+
hook = resource_config.get_callable('before_import')
|
|
217
|
+
if hook:
|
|
218
|
+
resource_attrs['before_import'] = hook
|
|
219
|
+
|
|
220
|
+
# after_import hook
|
|
221
|
+
if resource_config.after_import:
|
|
222
|
+
hook = resource_config.get_callable('after_import')
|
|
223
|
+
if hook:
|
|
224
|
+
resource_attrs['after_import'] = hook
|
|
225
|
+
|
|
226
|
+
# before_import_row hook
|
|
227
|
+
if resource_config.before_import_row:
|
|
228
|
+
hook = resource_config.get_callable('before_import_row')
|
|
229
|
+
if hook:
|
|
230
|
+
resource_attrs['before_import_row'] = hook
|
|
231
|
+
|
|
232
|
+
# after_import_row hook
|
|
233
|
+
if resource_config.after_import_row:
|
|
234
|
+
hook = resource_config.get_callable('after_import_row')
|
|
235
|
+
if hook:
|
|
236
|
+
resource_attrs['after_import_row'] = hook
|
|
237
|
+
|
|
238
|
+
# Create Resource class
|
|
239
|
+
AutoGeneratedResource = type(
|
|
240
|
+
f'{target_model.__name__}Resource',
|
|
241
|
+
(resources.ModelResource,),
|
|
242
|
+
resource_attrs
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
return AutoGeneratedResource
|
|
127
246
|
|
|
128
247
|
@classmethod
|
|
129
248
|
def _build_list_display(cls, config: AdminConfig) -> List[str]:
|
|
@@ -4,6 +4,7 @@ Configuration models for declarative Django Admin.
|
|
|
4
4
|
|
|
5
5
|
from .action_config import ActionConfig
|
|
6
6
|
from .admin_config import AdminConfig
|
|
7
|
+
from .background_task_config import BackgroundTaskConfig
|
|
7
8
|
from .field_config import (
|
|
8
9
|
FieldConfig,
|
|
9
10
|
BadgeField,
|
|
@@ -15,12 +16,15 @@ from .field_config import (
|
|
|
15
16
|
UserField,
|
|
16
17
|
)
|
|
17
18
|
from .fieldset_config import FieldsetConfig
|
|
19
|
+
from .resource_config import ResourceConfig
|
|
18
20
|
|
|
19
21
|
__all__ = [
|
|
20
22
|
"AdminConfig",
|
|
21
23
|
"FieldConfig",
|
|
22
24
|
"FieldsetConfig",
|
|
23
25
|
"ActionConfig",
|
|
26
|
+
"ResourceConfig",
|
|
27
|
+
"BackgroundTaskConfig",
|
|
24
28
|
# Specialized Field Types
|
|
25
29
|
"BadgeField",
|
|
26
30
|
"BooleanField",
|
|
@@ -8,8 +8,10 @@ from django.db import models
|
|
|
8
8
|
from pydantic import BaseModel, ConfigDict, Field
|
|
9
9
|
|
|
10
10
|
from .action_config import ActionConfig
|
|
11
|
+
from .background_task_config import BackgroundTaskConfig
|
|
11
12
|
from .field_config import FieldConfig
|
|
12
13
|
from .fieldset_config import FieldsetConfig
|
|
14
|
+
from .resource_config import ResourceConfig
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
class AdminConfig(BaseModel):
|
|
@@ -127,7 +129,17 @@ class AdminConfig(BaseModel):
|
|
|
127
129
|
|
|
128
130
|
# Import/Export options
|
|
129
131
|
import_export_enabled: bool = Field(False, description="Enable import/export functionality")
|
|
130
|
-
resource_class: Optional[Type] = Field(None, description="Resource class for import/export")
|
|
132
|
+
resource_class: Optional[Type] = Field(None, description="Custom Resource class for import/export")
|
|
133
|
+
resource_config: Optional[ResourceConfig] = Field(
|
|
134
|
+
None,
|
|
135
|
+
description="Declarative resource configuration (alternative to resource_class)"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Background task processing
|
|
139
|
+
background_task_config: Optional[BackgroundTaskConfig] = Field(
|
|
140
|
+
None,
|
|
141
|
+
description="Configuration for background task processing"
|
|
142
|
+
)
|
|
131
143
|
|
|
132
144
|
def get_display_field_config(self, field_name: str) -> Optional[FieldConfig]:
|
|
133
145
|
"""Get FieldConfig for a specific field."""
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Background task configuration for async operations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Literal, Optional
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BackgroundTaskConfig(BaseModel):
|
|
11
|
+
"""
|
|
12
|
+
Configuration for background task processing.
|
|
13
|
+
|
|
14
|
+
Used for async operations like testing imported items,
|
|
15
|
+
bulk processing, or long-running operations.
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
```python
|
|
19
|
+
background_config = BackgroundTaskConfig(
|
|
20
|
+
enabled=True,
|
|
21
|
+
task_runner='rearq',
|
|
22
|
+
batch_size=100,
|
|
23
|
+
timeout=300,
|
|
24
|
+
)
|
|
25
|
+
```
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
model_config = ConfigDict(
|
|
29
|
+
validate_assignment=True,
|
|
30
|
+
extra="forbid"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
enabled: bool = Field(
|
|
34
|
+
True,
|
|
35
|
+
description="Enable background task processing"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
task_runner: Literal['rearq', 'celery', 'django_q', 'sync'] = Field(
|
|
39
|
+
'rearq',
|
|
40
|
+
description="Task runner to use for background operations"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
batch_size: int = Field(
|
|
44
|
+
100,
|
|
45
|
+
ge=1,
|
|
46
|
+
le=10000,
|
|
47
|
+
description="Number of items to process in each batch"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
timeout: int = Field(
|
|
51
|
+
300,
|
|
52
|
+
ge=1,
|
|
53
|
+
le=3600,
|
|
54
|
+
description="Task timeout in seconds (max 1 hour)"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
retry_on_failure: bool = Field(
|
|
58
|
+
True,
|
|
59
|
+
description="Automatically retry failed tasks"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
max_retries: int = Field(
|
|
63
|
+
3,
|
|
64
|
+
ge=0,
|
|
65
|
+
le=10,
|
|
66
|
+
description="Maximum number of retry attempts"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
priority: Literal['high', 'normal', 'low'] = Field(
|
|
70
|
+
'normal',
|
|
71
|
+
description="Task priority in queue"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def should_use_background(self) -> bool:
|
|
75
|
+
"""Check if background processing should be used."""
|
|
76
|
+
return self.enabled and self.task_runner != 'sync'
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Resource configuration for import/export functionality.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ResourceConfig(BaseModel):
|
|
11
|
+
"""
|
|
12
|
+
Configuration for django-import-export Resource class.
|
|
13
|
+
|
|
14
|
+
Provides declarative configuration for import/export behavior without
|
|
15
|
+
needing to manually create Resource classes.
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
```python
|
|
19
|
+
resource_config = ResourceConfig(
|
|
20
|
+
fields=['host', 'port', 'username', 'password', 'provider'],
|
|
21
|
+
exclude=['metadata', 'config'],
|
|
22
|
+
import_id_fields=['host', 'port'],
|
|
23
|
+
after_import_row='apps.proxies.services.test_proxy_async',
|
|
24
|
+
skip_unchanged=True,
|
|
25
|
+
)
|
|
26
|
+
```
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
model_config = ConfigDict(
|
|
30
|
+
validate_assignment=True,
|
|
31
|
+
extra="forbid",
|
|
32
|
+
arbitrary_types_allowed=True
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Field configuration
|
|
36
|
+
fields: List[str] = Field(
|
|
37
|
+
default_factory=list,
|
|
38
|
+
description="Fields to include in import/export (empty = all fields)"
|
|
39
|
+
)
|
|
40
|
+
exclude: List[str] = Field(
|
|
41
|
+
default_factory=list,
|
|
42
|
+
description="Fields to exclude from import/export"
|
|
43
|
+
)
|
|
44
|
+
import_id_fields: List[str] = Field(
|
|
45
|
+
default_factory=lambda: ['id'],
|
|
46
|
+
description="Fields used to identify existing rows during import"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Import behavior
|
|
50
|
+
skip_unchanged: bool = Field(
|
|
51
|
+
True,
|
|
52
|
+
description="Skip rows that haven't changed during import"
|
|
53
|
+
)
|
|
54
|
+
report_skipped: bool = Field(
|
|
55
|
+
True,
|
|
56
|
+
description="Include skipped rows in import report"
|
|
57
|
+
)
|
|
58
|
+
skip_diff: bool = Field(
|
|
59
|
+
False,
|
|
60
|
+
description="Skip diff generation for faster imports (large datasets)"
|
|
61
|
+
)
|
|
62
|
+
use_transactions: bool = Field(
|
|
63
|
+
True,
|
|
64
|
+
description="Use database transactions for imports"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Validation and hooks (can be string paths or callables)
|
|
68
|
+
before_import: Optional[Union[str, Callable]] = Field(
|
|
69
|
+
None,
|
|
70
|
+
description="Hook called before import starts (receives dataset, dry_run)"
|
|
71
|
+
)
|
|
72
|
+
after_import: Optional[Union[str, Callable]] = Field(
|
|
73
|
+
None,
|
|
74
|
+
description="Hook called after import completes (receives dataset, result, dry_run)"
|
|
75
|
+
)
|
|
76
|
+
before_import_row: Optional[Union[str, Callable]] = Field(
|
|
77
|
+
None,
|
|
78
|
+
description="Hook called before each row import (receives row, row_number, dry_run)"
|
|
79
|
+
)
|
|
80
|
+
after_import_row: Optional[Union[str, Callable]] = Field(
|
|
81
|
+
None,
|
|
82
|
+
description="Hook called after each row import (receives row, row_result, row_number)"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Export options
|
|
86
|
+
export_order: List[str] = Field(
|
|
87
|
+
default_factory=list,
|
|
88
|
+
description="Field order in exported files (empty = model field order)"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Field widgets/customization
|
|
92
|
+
field_widgets: Dict[str, Any] = Field(
|
|
93
|
+
default_factory=dict,
|
|
94
|
+
description="Custom widgets for fields (e.g., {'date': {'format': '%Y-%m-%d'}})"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Batch processing
|
|
98
|
+
batch_size: Optional[int] = Field(
|
|
99
|
+
None,
|
|
100
|
+
description="Process imports in batches (for large datasets)"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def get_callable(self, hook_name: str) -> Optional[Callable]:
|
|
104
|
+
"""
|
|
105
|
+
Get callable for a hook by name.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
hook_name: Name of the hook ('before_import', 'after_import_row', etc.)
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Callable function or None if not set
|
|
112
|
+
"""
|
|
113
|
+
hook_value = getattr(self, hook_name, None)
|
|
114
|
+
|
|
115
|
+
if hook_value is None:
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
# If already a callable, return it
|
|
119
|
+
if callable(hook_value):
|
|
120
|
+
return hook_value
|
|
121
|
+
|
|
122
|
+
# If string, import it
|
|
123
|
+
if isinstance(hook_value, str):
|
|
124
|
+
import importlib
|
|
125
|
+
module_path, function_name = hook_value.rsplit('.', 1)
|
|
126
|
+
module = importlib.import_module(module_path)
|
|
127
|
+
return getattr(module, function_name)
|
|
128
|
+
|
|
129
|
+
return None
|
|
@@ -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,13 +4,13 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "django-cfg"
|
|
7
|
-
version = "1.4.
|
|
7
|
+
version = "1.4.107"
|
|
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",]
|
|
11
11
|
classifiers = [ "Development Status :: 4 - Beta", "Framework :: Django", "Framework :: Django :: 5.2", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Systems Administration", "Typing :: Typed",]
|
|
12
12
|
requires-python = ">=3.12,<3.14"
|
|
13
|
-
dependencies = [ "pydantic>=2.11.0,<3.0", "pydantic[email]>=2.11.0,<3.0", "PyYAML>=6.0,<7.0", "click>=8.2.0,<9.0", "questionary>=2.1.0,<3.0", "rich>=14.0.0,<15.0", "cloudflare>=4.3.0,<5.0", "loguru>=0.7.0,<1.0", "colorlog>=6.9.0,<7.0", "cachetools>=5.3.0,<7.0", "toml>=0.10.2,<0.11.0", "ngrok>=1.5.1; python_version>='3.12'", "psycopg[binary,pool]>=3.2.0,<4.0", "dj-database-url>=3.0.0,<4.0", "whitenoise>=6.8.0,<7.0", "django-cors-headers>=4.7.0,<5.0", "djangorestframework>=3.16.0,<4.0", "djangorestframework-simplejwt>=5.5.0,<6.0", "djangorestframework-simplejwt[token-blacklist]>=5.5.0,<6.0", "drf-nested-routers>=0.94.0,<1.0", "django-filter>=25.0,<26.0", "django-ratelimit>=4.1.0,<5.0.0", "drf-spectacular>=0.28.0,<1.0", "drf-spectacular-sidecar>=2025.8.0,<2026.0", "django-json-widget>=2.0.0,<3.0", "django-import-export>=4.3.0,<5.0", "django-extensions>=4.1.0,<5.0", "django-constance>=4.3.0,<5.0", "django-unfold>=0.64.0,<1.0", "django-redis>=6.0.0,<7.0", "redis>=6.4.0,<7.0", "hiredis>=2.0.0,<4.0", "dramatiq[redis]>=1.18.0,<2.0", "django-dramatiq>=0.14.0,<1.0", "pyTelegramBotAPI>=4.28.0,<5.0", "coolname>=2.2.0,<3.0", "django-admin-rangefilter>=0.13.0,<1.0", "python-json-logger>=3.3.0,<4.0", "requests>=2.32.0,<3.0", "tiktoken>=0.11.0,<1.0", "openai>=1.107.0,<2.0", "twilio>=9.8.0,<10.0", "sendgrid>=6.12.0,<7.0", "beautifulsoup4>=4.13.0,<5.0", "lxml>=6.0.0,<7.0", "pgvector>=0.4.0,<1.0", "tenacity>=9.1.2,<10.0.0", "mypy (>=1.18.2,<2.0.0)", "django-tailwind[reload] (>=4.2.0,<5.0.0)", "jinja2 (>=3.1.6,<4.0.0)", "django-axes[ipware] (>=8.0.0,<9.0.0)", "pydantic-settings (>=2.11.0,<3.0.0)",]
|
|
13
|
+
dependencies = [ "pydantic>=2.11.0,<3.0", "pydantic[email]>=2.11.0,<3.0", "PyYAML>=6.0,<7.0", "click>=8.2.0,<9.0", "questionary>=2.1.0,<3.0", "rich>=14.0.0,<15.0", "cloudflare>=4.3.0,<5.0", "loguru>=0.7.0,<1.0", "colorlog>=6.9.0,<7.0", "cachetools>=5.3.0,<7.0", "toml>=0.10.2,<0.11.0", "ngrok>=1.5.1; python_version>='3.12'", "psycopg[binary,pool]>=3.2.0,<4.0", "dj-database-url>=3.0.0,<4.0", "whitenoise>=6.8.0,<7.0", "django-cors-headers>=4.7.0,<5.0", "djangorestframework>=3.16.0,<4.0", "djangorestframework-simplejwt>=5.5.0,<6.0", "djangorestframework-simplejwt[token-blacklist]>=5.5.0,<6.0", "drf-nested-routers>=0.94.0,<1.0", "django-filter>=25.0,<26.0", "django-ratelimit>=4.1.0,<5.0.0", "drf-spectacular>=0.28.0,<1.0", "drf-spectacular-sidecar>=2025.8.0,<2026.0", "django-json-widget>=2.0.0,<3.0", "django-import-export>=4.3.0,<5.0", "django-extensions>=4.1.0,<5.0", "django-constance>=4.3.0,<5.0", "django-unfold>=0.64.0,<1.0", "django-redis>=6.0.0,<7.0", "redis>=6.4.0,<7.0", "hiredis>=2.0.0,<4.0", "dramatiq[redis]>=1.18.0,<2.0", "django-dramatiq>=0.14.0,<1.0", "pyTelegramBotAPI>=4.28.0,<5.0", "coolname>=2.2.0,<3.0", "django-admin-rangefilter>=0.13.0,<1.0", "python-json-logger>=3.3.0,<4.0", "requests>=2.32.0,<3.0", "tiktoken>=0.11.0,<1.0", "openai>=1.107.0,<2.0", "twilio>=9.8.0,<10.0", "sendgrid>=6.12.0,<7.0", "beautifulsoup4>=4.13.0,<5.0", "lxml>=6.0.0,<7.0", "pgvector>=0.4.0,<1.0", "tenacity>=9.1.2,<10.0.0", "mypy (>=1.18.2,<2.0.0)", "django-tailwind[reload] (>=4.2.0,<5.0.0)", "jinja2 (>=3.1.6,<4.0.0)", "django-axes[ipware] (>=8.0.0,<9.0.0)", "pydantic-settings (>=2.11.0,<3.0.0)", "pytz>=2025.1", "httpx>=0.28.1,<1.0",]
|
|
14
14
|
[[project.authors]]
|
|
15
15
|
name = "Django-CFG Team"
|
|
16
16
|
email = "info@djangocfg.com"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-cfg
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.107
|
|
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
|
|
@@ -56,6 +56,7 @@ Requires-Dist: drf-nested-routers<1.0,>=0.94.0
|
|
|
56
56
|
Requires-Dist: drf-spectacular-sidecar<2026.0,>=2025.8.0
|
|
57
57
|
Requires-Dist: drf-spectacular<1.0,>=0.28.0
|
|
58
58
|
Requires-Dist: hiredis<4.0,>=2.0.0
|
|
59
|
+
Requires-Dist: httpx<1.0,>=0.28.1
|
|
59
60
|
Requires-Dist: jinja2<4.0.0,>=3.1.6
|
|
60
61
|
Requires-Dist: loguru<1.0,>=0.7.0
|
|
61
62
|
Requires-Dist: lxml<7.0,>=6.0.0
|
|
@@ -69,6 +70,7 @@ Requires-Dist: pydantic<3.0,>=2.11.0
|
|
|
69
70
|
Requires-Dist: pydantic[email]<3.0,>=2.11.0
|
|
70
71
|
Requires-Dist: pytelegrambotapi<5.0,>=4.28.0
|
|
71
72
|
Requires-Dist: python-json-logger<4.0,>=3.3.0
|
|
73
|
+
Requires-Dist: pytz>=2025.1
|
|
72
74
|
Requires-Dist: pyyaml<7.0,>=6.0
|
|
73
75
|
Requires-Dist: questionary<3.0,>=2.1.0
|
|
74
76
|
Requires-Dist: redis<7.0,>=6.4.0
|
|
@@ -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=KYlYkP_W1VDKzLgBVLlALZylOST7Myun4rcy-Sa-_80,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,15 +695,19 @@ 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/
|
|
698
|
+
django_cfg/modules/django_admin/IMPORT_EXPORT_FIX.md,sha256=F8z4oHZk7jkRChxW8tKsVf0Q_OujjlBUs8UJNirn55Y,2055
|
|
699
|
+
django_cfg/modules/django_admin/RESOURCE_CONFIG_ENHANCEMENT.md,sha256=MKUpRHMqM3KDEKZthWhul-4EoSnE75kkWd7IcEzwYDQ,8729
|
|
700
|
+
django_cfg/modules/django_admin/__init__.py,sha256=LYVFCVgN6Irr80dK8SkXgKQ-Xgg2MyRL5CgfOgmSt8A,3190
|
|
699
701
|
django_cfg/modules/django_admin/base/__init__.py,sha256=tzre09bnD_SlS-pA30WzYZRxyvch7eLq3q0wLEcZOmc,118
|
|
700
|
-
django_cfg/modules/django_admin/base/pydantic_admin.py,sha256=
|
|
702
|
+
django_cfg/modules/django_admin/base/pydantic_admin.py,sha256=v2U-Tm_zWkDsh1CGmYL-xJdsVvHKX7MjUMrFwpY6wpw,20879
|
|
701
703
|
django_cfg/modules/django_admin/base/unfold_admin.py,sha256=iqpRWSkzW5HktXDuuG7G3J6RoIfW48dWPMJTa7Yk08g,729
|
|
702
|
-
django_cfg/modules/django_admin/config/__init__.py,sha256=
|
|
704
|
+
django_cfg/modules/django_admin/config/__init__.py,sha256=IjD2pJBb_rboj9V4_4uzBmzIIzm4a5Xy8XIieL5myww,755
|
|
703
705
|
django_cfg/modules/django_admin/config/action_config.py,sha256=JjS01JxLT-FzUVq7RlKaB7L38wmVL8uibXO_iXZcljo,1668
|
|
704
|
-
django_cfg/modules/django_admin/config/admin_config.py,sha256=
|
|
706
|
+
django_cfg/modules/django_admin/config/admin_config.py,sha256=7ozVnLqJcUrHu-UK-PdOfuX2UOvR1gkigThgQsfO0wY,5030
|
|
707
|
+
django_cfg/modules/django_admin/config/background_task_config.py,sha256=7-8B1rhpeafanVxtjFQUx0mVjcA5xmxZIxqKzaBwMX0,1760
|
|
705
708
|
django_cfg/modules/django_admin/config/field_config.py,sha256=TXdmz1GHC0N7euzqgB9p5EhFCYzsEobb8VSgS4kG4ho,8928
|
|
706
709
|
django_cfg/modules/django_admin/config/fieldset_config.py,sha256=5BPUWO_HvS6YhPU_vqQPzRT2y3OIPrBCioFuer5-mrA,1249
|
|
710
|
+
django_cfg/modules/django_admin/config/resource_config.py,sha256=6ELWSuHlrpNqV2wO2sL_o2mE2DXLd_solcsdN5O6eQg,3962
|
|
707
711
|
django_cfg/modules/django_admin/icons/README.md,sha256=j-NUixSC1QJh7PqYKxLZpPrTxKrAnx0urQraXgv4JHI,5612
|
|
708
712
|
django_cfg/modules/django_admin/icons/__init__.py,sha256=XrdQ3vmADQ1y2yNaL2jvffHCXjy3GjpoBbTsDPLD8Ko,198
|
|
709
713
|
django_cfg/modules/django_admin/icons/constants.py,sha256=g5rrZOMLP4bT824pwksRavNQsW1I2AdWKvoGPtk5qQA,124554
|
|
@@ -909,8 +913,14 @@ django_cfg/modules/django_email/management/commands/__init__.py,sha256=47DEQpj8H
|
|
|
909
913
|
django_cfg/modules/django_email/management/commands/test_email.py,sha256=PecBUXIkT0tlH6_ZbhLOrju_lKCnsfICuFlWBpXIyGk,3316
|
|
910
914
|
django_cfg/modules/django_health/__init__.py,sha256=lX8uJlAmXZeAVfQy12H67uTTy5SOjYMu3N8d8WF8ZNM,156
|
|
911
915
|
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=
|
|
916
|
+
django_cfg/modules/django_import_export/README.md,sha256=Oha18L7skITg-5oOfcKEvwealrD7FIziXt2y66WoaYM,3115
|
|
917
|
+
django_cfg/modules/django_import_export/__init__.py,sha256=vnMEpayU1RCmsC3yC38oIRvZzlmcLg0d0Y24fzQbl0Y,2418
|
|
918
|
+
django_cfg/modules/django_import_export/apps.py,sha256=W_yhcefsNxz8aYFCwacFmeFeTZ0clUTGU0gL-GfGOQ4,560
|
|
919
|
+
django_cfg/modules/django_import_export/templates/admin/import_export/change_list_export.html,sha256=RcbmcksrwUtZVA_hKszu2pMOPw3I758qsNWMNJSrDJk,177
|
|
920
|
+
django_cfg/modules/django_import_export/templates/admin/import_export/change_list_export_item.html,sha256=Lq_c0KS-FEbG7eCo6XjUSgB-Mp9XOr_m7Mn77Z2f_CA,330
|
|
921
|
+
django_cfg/modules/django_import_export/templates/admin/import_export/change_list_import.html,sha256=XW89hAnilxQT2TKuIpQ4SPtxcdnRMl_1X_h_Ie5V2Xc,177
|
|
922
|
+
django_cfg/modules/django_import_export/templates/admin/import_export/change_list_import_export.html,sha256=BHr4h4aX7_ZzodfwisVbAewf8qKOuhGNeseURcMmK7o,1269
|
|
923
|
+
django_cfg/modules/django_import_export/templates/admin/import_export/change_list_import_item.html,sha256=XYsTEbQc3CCQR06-4qazXx-svQ0RoEDE6YPKoZw1oKo,305
|
|
914
924
|
django_cfg/modules/django_llm/README.md,sha256=31uQG9_z-HroEUevtOPJbZ86H9yiwzZJs0omf-Kwh-o,9374
|
|
915
925
|
django_cfg/modules/django_llm/__init__.py,sha256=tksyeH6e5ajrSNa6MVkeu_IZQE_I-zka1iTLAIU-WtE,1913
|
|
916
926
|
django_cfg/modules/django_llm/example.py,sha256=PqOZXV1qY8GjFfRdOM3a8Rjj59CzDbETokkvPqAkkm8,12797
|
|
@@ -1087,9 +1097,9 @@ django_cfg/utils/version_check.py,sha256=WO51J2m2e-wVqWCRwbultEwu3q1lQasV67Mw2aa
|
|
|
1087
1097
|
django_cfg/CHANGELOG.md,sha256=jtT3EprqEJkqSUh7IraP73vQ8PmKUMdRtznQsEnqDZk,2052
|
|
1088
1098
|
django_cfg/CONTRIBUTING.md,sha256=DU2kyQ6PU0Z24ob7O_OqKWEYHcZmJDgzw-lQCmu6uBg,3041
|
|
1089
1099
|
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.
|
|
1100
|
+
django_cfg/pyproject.toml,sha256=ejHs8Z6myjIm2T37CZ34T6AZZsJQokkCICbzWwZTS4s,8611
|
|
1101
|
+
django_cfg-1.4.107.dist-info/METADATA,sha256=1wc8DsLeaO5VvXTAWpExOOqhKuT7Q_LXw4NNAp4znOo,23796
|
|
1102
|
+
django_cfg-1.4.107.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
1103
|
+
django_cfg-1.4.107.dist-info/entry_points.txt,sha256=Ucmde4Z2wEzgb4AggxxZ0zaYDb9HpyE5blM3uJ0_VNg,56
|
|
1104
|
+
django_cfg-1.4.107.dist-info/licenses/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
|
|
1105
|
+
django_cfg-1.4.107.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|