django-cfg 1.2.20__py3-none-any.whl → 1.2.21__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.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/maintenance/admin/site_admin.py +47 -8
- django_cfg/apps/maintenance/migrations/0003_cloudflaresite_include_subdomains_and_more.py +27 -0
- django_cfg/apps/maintenance/models/cloudflare_site.py +41 -0
- django_cfg/apps/maintenance/services/maintenance_service.py +121 -32
- django_cfg/apps/maintenance/services/site_sync_service.py +56 -11
- django_cfg/core/config.py +20 -1
- django_cfg/core/generation.py +2 -1
- django_cfg/management/commands/check_settings.py +54 -0
- django_cfg/utils/smart_defaults.py +211 -85
- {django_cfg-1.2.20.dist-info → django_cfg-1.2.21.dist-info}/METADATA +1 -1
- {django_cfg-1.2.20.dist-info → django_cfg-1.2.21.dist-info}/RECORD +15 -14
- {django_cfg-1.2.20.dist-info → django_cfg-1.2.21.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.20.dist-info → django_cfg-1.2.21.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.20.dist-info → django_cfg-1.2.21.dist-info}/licenses/LICENSE +0 -0
django_cfg/__init__.py
CHANGED
@@ -73,6 +73,7 @@ class CloudflareSiteAdmin(ModelAdmin):
|
|
73
73
|
'status_display',
|
74
74
|
'name',
|
75
75
|
'domain',
|
76
|
+
'subdomain_config_badge',
|
76
77
|
'maintenance_badge',
|
77
78
|
'active_badge',
|
78
79
|
'last_maintenance_at',
|
@@ -102,6 +103,10 @@ class CloudflareSiteAdmin(ModelAdmin):
|
|
102
103
|
('Basic Information', {
|
103
104
|
'fields': ['name', 'domain']
|
104
105
|
}),
|
106
|
+
('Subdomain Configuration', {
|
107
|
+
'fields': ['include_subdomains', 'subdomain_list'],
|
108
|
+
'description': 'Configure which subdomains should be affected by maintenance mode'
|
109
|
+
}),
|
105
110
|
('Cloudflare Configuration', {
|
106
111
|
'fields': ['zone_id', 'account_id', 'api_key'],
|
107
112
|
'classes': ['collapse']
|
@@ -141,6 +146,32 @@ class CloudflareSiteAdmin(ModelAdmin):
|
|
141
146
|
# Display methods
|
142
147
|
|
143
148
|
@display(description="Status")
|
149
|
+
@display(description="Subdomains", ordering='include_subdomains')
|
150
|
+
def subdomain_config_badge(self, obj: CloudflareSite) -> str:
|
151
|
+
"""Display subdomain configuration with badge."""
|
152
|
+
config = obj.get_subdomain_display()
|
153
|
+
|
154
|
+
if obj.include_subdomains:
|
155
|
+
# All subdomains
|
156
|
+
badge_class = "badge-success"
|
157
|
+
icon = "🌐"
|
158
|
+
elif obj.subdomain_list.strip():
|
159
|
+
# Specific subdomains
|
160
|
+
badge_class = "badge-warning"
|
161
|
+
icon = "📋"
|
162
|
+
else:
|
163
|
+
# Root only
|
164
|
+
badge_class = "badge-secondary"
|
165
|
+
icon = "🏠"
|
166
|
+
|
167
|
+
return format_html(
|
168
|
+
'<span class="badge {}" title="{}">{} {}</span>',
|
169
|
+
badge_class,
|
170
|
+
config,
|
171
|
+
icon,
|
172
|
+
"All" if obj.include_subdomains else ("List" if obj.subdomain_list.strip() else "Root")
|
173
|
+
)
|
174
|
+
|
144
175
|
def status_display(self, obj: CloudflareSite) -> str:
|
145
176
|
"""Display status with emoji."""
|
146
177
|
if obj.maintenance_active:
|
@@ -299,6 +330,7 @@ class CloudflareSiteAdmin(ModelAdmin):
|
|
299
330
|
|
300
331
|
@action(
|
301
332
|
description="🔄 Sync with Cloudflare",
|
333
|
+
url_path="sync-cloudflare",
|
302
334
|
icon="refresh",
|
303
335
|
variant=ActionVariant.INFO
|
304
336
|
)
|
@@ -310,15 +342,20 @@ class CloudflareSiteAdmin(ModelAdmin):
|
|
310
342
|
messages.error(request, "Site not found.")
|
311
343
|
return redirect(request.META.get('HTTP_REFERER', '/admin/'))
|
312
344
|
|
313
|
-
# Use
|
314
|
-
from ..services import
|
315
|
-
|
316
|
-
sync_service.sync_zones()
|
345
|
+
# Use convenience function for single site sync
|
346
|
+
from ..services import sync_site_from_cloudflare
|
347
|
+
log_entry = sync_site_from_cloudflare(site)
|
317
348
|
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
349
|
+
if log_entry.status == log_entry.Status.SUCCESS:
|
350
|
+
messages.success(
|
351
|
+
request,
|
352
|
+
f"Site '{site.name}' has been synchronized with Cloudflare."
|
353
|
+
)
|
354
|
+
else:
|
355
|
+
messages.error(
|
356
|
+
request,
|
357
|
+
f"Failed to sync site '{site.name}': {log_entry.error_message}"
|
358
|
+
)
|
322
359
|
|
323
360
|
except Exception as e:
|
324
361
|
messages.error(request, f"Failed to sync site: {str(e)}")
|
@@ -327,6 +364,7 @@ class CloudflareSiteAdmin(ModelAdmin):
|
|
327
364
|
|
328
365
|
@action(
|
329
366
|
description="🔧 Enable Maintenance",
|
367
|
+
url_path="enable-maintenance",
|
330
368
|
icon="build",
|
331
369
|
variant=ActionVariant.WARNING
|
332
370
|
)
|
@@ -350,6 +388,7 @@ class CloudflareSiteAdmin(ModelAdmin):
|
|
350
388
|
|
351
389
|
@action(
|
352
390
|
description="✅ Disable Maintenance",
|
391
|
+
url_path="disable-maintenance",
|
353
392
|
icon="check_circle",
|
354
393
|
variant=ActionVariant.SUCCESS
|
355
394
|
)
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# Generated by Django 5.2.6 on 2025-09-23 09:43
|
2
|
+
|
3
|
+
from django.db import migrations, models
|
4
|
+
|
5
|
+
|
6
|
+
class Migration(migrations.Migration):
|
7
|
+
dependencies = [
|
8
|
+
("maintenance", "0002_cloudflaresite_maintenance_url"),
|
9
|
+
]
|
10
|
+
|
11
|
+
operations = [
|
12
|
+
migrations.AddField(
|
13
|
+
model_name="cloudflaresite",
|
14
|
+
name="include_subdomains",
|
15
|
+
field=models.BooleanField(
|
16
|
+
default=True, help_text="Apply maintenance rules to all subdomains (*.domain.com)"
|
17
|
+
),
|
18
|
+
),
|
19
|
+
migrations.AddField(
|
20
|
+
model_name="cloudflaresite",
|
21
|
+
name="subdomain_list",
|
22
|
+
field=models.TextField(
|
23
|
+
blank=True,
|
24
|
+
help_text="Comma-separated list of specific subdomains to include (e.g., api,www,app)",
|
25
|
+
),
|
26
|
+
),
|
27
|
+
]
|
@@ -29,6 +29,16 @@ class CloudflareSite(models.Model):
|
|
29
29
|
help_text="Domain name (e.g., vamcar.com)"
|
30
30
|
)
|
31
31
|
|
32
|
+
# Subdomains configuration
|
33
|
+
include_subdomains = models.BooleanField(
|
34
|
+
default=True,
|
35
|
+
help_text="Apply maintenance rules to all subdomains (*.domain.com)"
|
36
|
+
)
|
37
|
+
subdomain_list = models.TextField(
|
38
|
+
blank=True,
|
39
|
+
help_text="Comma-separated list of specific subdomains to include (e.g., api,www,app)"
|
40
|
+
)
|
41
|
+
|
32
42
|
# Cloudflare IDs (auto-discovered during sync)
|
33
43
|
zone_id = models.CharField(
|
34
44
|
max_length=32,
|
@@ -106,6 +116,37 @@ class CloudflareSite(models.Model):
|
|
106
116
|
# Default maintenance page with site parameter
|
107
117
|
return get_maintenance_url(self.domain)
|
108
118
|
|
119
|
+
def get_domain_patterns(self) -> list[str]:
|
120
|
+
"""Get list of domain patterns for Page Rules based on subdomain configuration."""
|
121
|
+
patterns = []
|
122
|
+
|
123
|
+
if self.include_subdomains:
|
124
|
+
# Include all subdomains with wildcard
|
125
|
+
patterns.append(f"*{self.domain}/*")
|
126
|
+
# Also include root domain explicitly
|
127
|
+
patterns.append(f"{self.domain}/*")
|
128
|
+
else:
|
129
|
+
# Only root domain
|
130
|
+
patterns.append(f"{self.domain}/*")
|
131
|
+
|
132
|
+
# Add specific subdomains if specified
|
133
|
+
if self.subdomain_list.strip():
|
134
|
+
subdomains = [sub.strip() for sub in self.subdomain_list.split(',') if sub.strip()]
|
135
|
+
for subdomain in subdomains:
|
136
|
+
patterns.append(f"{subdomain}.{self.domain}/*")
|
137
|
+
|
138
|
+
return patterns
|
139
|
+
|
140
|
+
def get_subdomain_display(self) -> str:
|
141
|
+
"""Get human-readable subdomain configuration."""
|
142
|
+
if self.include_subdomains:
|
143
|
+
return f"All subdomains (*.{self.domain})"
|
144
|
+
elif self.subdomain_list.strip():
|
145
|
+
subdomains = [sub.strip() for sub in self.subdomain_list.split(',') if sub.strip()]
|
146
|
+
return f"Specific: {', '.join(subdomains)}.{self.domain}"
|
147
|
+
else:
|
148
|
+
return f"Root domain only ({self.domain})"
|
149
|
+
|
109
150
|
def clean(self) -> None:
|
110
151
|
"""Validate model data."""
|
111
152
|
super().clean()
|
@@ -142,36 +142,87 @@ class MaintenanceService:
|
|
142
142
|
|
143
143
|
@retry_on_failure(max_retries=3, base_delay=1.0)
|
144
144
|
def _create_maintenance_page_rule(self) -> Dict[str, Any]:
|
145
|
-
"""Create Page
|
145
|
+
"""Create Page Rules to redirect all traffic to maintenance page (including subdomains)."""
|
146
146
|
maintenance_url = self.site.get_maintenance_url()
|
147
|
-
|
147
|
+
patterns = self.site.get_domain_patterns()
|
148
148
|
|
149
|
-
logger.info(f"Creating page
|
149
|
+
logger.info(f"Creating page rules for {self.site.domain}: {patterns} → {maintenance_url}")
|
150
150
|
|
151
|
-
|
151
|
+
# First, check if conflicting Page Rules already exist and delete them
|
152
|
+
try:
|
153
|
+
existing_rules = self.client.page_rules.list(zone_id=self.site.zone_id)
|
154
|
+
|
155
|
+
# Handle different API response formats
|
156
|
+
if hasattr(existing_rules, 'result'):
|
157
|
+
rules = existing_rules.result
|
158
|
+
else:
|
159
|
+
rules = existing_rules
|
160
|
+
|
161
|
+
# Look for conflicting rules with same patterns
|
162
|
+
for rule in rules:
|
163
|
+
if (hasattr(rule, 'targets') and rule.targets and
|
164
|
+
len(rule.targets) > 0 and
|
165
|
+
hasattr(rule.targets[0], 'constraint') and
|
166
|
+
hasattr(rule.targets[0].constraint, 'value')):
|
167
|
+
|
168
|
+
rule_pattern = rule.targets[0].constraint.value
|
169
|
+
if rule_pattern in patterns:
|
170
|
+
logger.info(f"Found conflicting page rule {rule.id} with pattern {rule_pattern}, deleting...")
|
171
|
+
self.client.page_rules.delete(
|
152
172
|
zone_id=self.site.zone_id,
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
}
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
173
|
+
pagerule_id=rule.id
|
174
|
+
)
|
175
|
+
logger.info(f"Deleted conflicting page rule {rule.id}")
|
176
|
+
|
177
|
+
except Exception as e:
|
178
|
+
logger.warning(f"Error checking existing page rules: {e}")
|
179
|
+
# Continue with creation anyway
|
180
|
+
|
181
|
+
# Create Page Rules for each pattern
|
182
|
+
created_rules = []
|
183
|
+
for pattern in patterns:
|
184
|
+
try:
|
185
|
+
logger.info(f"Creating page rule: {pattern} → {maintenance_url}")
|
186
|
+
response = self.client.page_rules.create(
|
187
|
+
zone_id=self.site.zone_id,
|
188
|
+
targets=[{
|
189
|
+
"target": "url",
|
190
|
+
"constraint": {
|
191
|
+
"operator": "matches",
|
192
|
+
"value": pattern
|
193
|
+
}
|
194
|
+
}],
|
195
|
+
actions=[{
|
196
|
+
"id": "forwarding_url",
|
197
|
+
"value": {
|
198
|
+
"url": maintenance_url,
|
199
|
+
"status_code": 302
|
200
|
+
}
|
201
|
+
}],
|
202
|
+
status="active"
|
203
|
+
)
|
204
|
+
created_rules.append({
|
205
|
+
"pattern": pattern,
|
206
|
+
"rule_id": response.id if hasattr(response, 'id') else 'unknown',
|
207
|
+
"response": response.model_dump()
|
208
|
+
})
|
209
|
+
logger.info(f"Created page rule for pattern {pattern}")
|
210
|
+
|
211
|
+
except Exception as e:
|
212
|
+
logger.error(f"Failed to create page rule for pattern {pattern}: {e}")
|
213
|
+
# Continue with other patterns
|
214
|
+
|
215
|
+
return {
|
216
|
+
"success": True,
|
217
|
+
"patterns": patterns,
|
218
|
+
"created_rules": created_rules,
|
219
|
+
"total_rules": len(created_rules)
|
220
|
+
}
|
170
221
|
|
171
222
|
@retry_on_failure(max_retries=3, base_delay=1.0)
|
172
223
|
def _delete_maintenance_page_rule(self) -> Dict[str, Any]:
|
173
|
-
"""Delete maintenance Page
|
174
|
-
# Find the maintenance page
|
224
|
+
"""Delete maintenance Page Rules with retry logic (including subdomains)."""
|
225
|
+
# Find the maintenance page rules
|
175
226
|
page_rules_response = self.client.page_rules.list(zone_id=self.site.zone_id)
|
176
227
|
|
177
228
|
# Handle different API response formats
|
@@ -180,37 +231,75 @@ class MaintenanceService:
|
|
180
231
|
else:
|
181
232
|
page_rules = page_rules_response
|
182
233
|
|
183
|
-
|
234
|
+
patterns = self.site.get_domain_patterns()
|
184
235
|
maintenance_url = self.site.get_maintenance_url()
|
185
236
|
|
186
|
-
logger.info(f"Looking for page
|
237
|
+
logger.info(f"Looking for page rules to delete: patterns={patterns}, url={maintenance_url}")
|
187
238
|
logger.info(f"Found {len(page_rules)} page rules total")
|
188
239
|
|
240
|
+
deleted_rules = []
|
241
|
+
|
189
242
|
for rule in page_rules:
|
190
243
|
logger.info(f"Checking rule {rule.id}: targets={getattr(rule, 'targets', None)}, actions={getattr(rule, 'actions', None)}")
|
191
244
|
|
192
|
-
#
|
193
|
-
|
245
|
+
# Check if this rule matches our maintenance patterns
|
246
|
+
rule_matches = False
|
247
|
+
|
248
|
+
# Check by pattern
|
249
|
+
if (hasattr(rule, 'targets') and rule.targets and
|
250
|
+
len(rule.targets) > 0 and
|
251
|
+
hasattr(rule.targets[0], 'constraint') and
|
252
|
+
hasattr(rule.targets[0].constraint, 'value')):
|
253
|
+
|
254
|
+
rule_pattern = rule.targets[0].constraint.value
|
255
|
+
if rule_pattern in patterns:
|
256
|
+
rule_matches = True
|
257
|
+
logger.info(f"Rule {rule.id} matches pattern: {rule_pattern}")
|
258
|
+
|
259
|
+
# Also check by URL (fallback for older rules)
|
260
|
+
if not rule_matches and (hasattr(rule, 'actions') and rule.actions and
|
194
261
|
len(rule.actions) > 0 and
|
195
262
|
hasattr(rule.actions[0], 'id') and
|
196
263
|
rule.actions[0].id == "forwarding_url"):
|
197
264
|
|
198
|
-
# Check if URL contains maintenance.reforms.ai
|
199
265
|
action_value = getattr(rule.actions[0], 'value', {})
|
200
266
|
action_url = getattr(action_value, 'url', '')
|
201
267
|
|
202
268
|
logger.info(f"Found forwarding rule with URL: {action_url}")
|
203
269
|
|
204
|
-
if "maintenance.reforms.ai" in action_url
|
270
|
+
if ("maintenance.reforms.ai" in action_url or
|
271
|
+
"djangocfg.com/maintenance" in action_url):
|
272
|
+
rule_matches = True
|
273
|
+
logger.info(f"Rule {rule.id} matches maintenance URL")
|
274
|
+
|
275
|
+
# Delete matching rule
|
276
|
+
if rule_matches:
|
277
|
+
try:
|
205
278
|
logger.info(f"Deleting maintenance page rule: {rule.id}")
|
206
279
|
response = self.client.page_rules.delete(
|
207
280
|
zone_id=self.site.zone_id,
|
208
281
|
pagerule_id=rule.id
|
209
282
|
)
|
210
|
-
|
283
|
+
deleted_rules.append({
|
284
|
+
"rule_id": rule.id,
|
285
|
+
"pattern": getattr(rule.targets[0].constraint, 'value', 'unknown') if hasattr(rule, 'targets') and rule.targets else 'unknown',
|
286
|
+
"response": response.model_dump()
|
287
|
+
})
|
288
|
+
logger.info(f"Successfully deleted page rule: {rule.id}")
|
289
|
+
|
290
|
+
except Exception as e:
|
291
|
+
logger.error(f"Failed to delete page rule {rule.id}: {e}")
|
211
292
|
|
212
|
-
|
213
|
-
|
293
|
+
if deleted_rules:
|
294
|
+
logger.info(f"Deleted {len(deleted_rules)} maintenance page rules for {self.site.domain}")
|
295
|
+
return {
|
296
|
+
"success": True,
|
297
|
+
"deleted_rules": deleted_rules,
|
298
|
+
"total_deleted": len(deleted_rules)
|
299
|
+
}
|
300
|
+
else:
|
301
|
+
logger.warning(f"No maintenance page rules found for {self.site.domain}")
|
302
|
+
return {"success": True, "message": "No page rules to delete"}
|
214
303
|
|
215
304
|
|
216
305
|
|
@@ -68,6 +68,7 @@ class SiteSyncService:
|
|
68
68
|
logger.error(f"Failed to discover zones: {e}")
|
69
69
|
raise CloudflareRetryError(f"Zone discovery failed: {e}")
|
70
70
|
|
71
|
+
|
71
72
|
def sync_zones(self,
|
72
73
|
force_update: bool = False,
|
73
74
|
dry_run: bool = False) -> Dict[str, Any]:
|
@@ -163,9 +164,18 @@ class SiteSyncService:
|
|
163
164
|
'changes': self._get_site_changes(site, zone_data)
|
164
165
|
}
|
165
166
|
else:
|
166
|
-
# Update existing site
|
167
|
+
# Update existing site - preserve subdomain settings
|
167
168
|
site.domain = domain
|
168
169
|
site.account_id = zone_data.get('account_id', site.account_id)
|
170
|
+
|
171
|
+
# Ensure subdomain fields are not reset to None during sync
|
172
|
+
if site.include_subdomains is None:
|
173
|
+
site.include_subdomains = True
|
174
|
+
if site.subdomain_list is None:
|
175
|
+
site.subdomain_list = ''
|
176
|
+
if site.maintenance_url is None:
|
177
|
+
site.maintenance_url = ''
|
178
|
+
|
169
179
|
site.save()
|
170
180
|
|
171
181
|
# Log the sync
|
@@ -195,14 +205,18 @@ class SiteSyncService:
|
|
195
205
|
'zone_data': zone_data
|
196
206
|
}
|
197
207
|
else:
|
198
|
-
# Create new site
|
208
|
+
# Create new site with default subdomain settings
|
199
209
|
site = CloudflareSite.objects.create(
|
200
210
|
name=self._generate_site_name(domain),
|
201
211
|
domain=domain,
|
202
212
|
zone_id=zone_id,
|
203
213
|
account_id=zone_data.get('account_id', ''),
|
204
214
|
api_key=self.api_key,
|
205
|
-
is_active=not zone_data.get('paused', False)
|
215
|
+
is_active=not zone_data.get('paused', False),
|
216
|
+
# Use default values from model
|
217
|
+
include_subdomains=True, # Default: include all subdomains
|
218
|
+
subdomain_list='', # Default: empty list
|
219
|
+
maintenance_url='' # Default: empty URL
|
206
220
|
)
|
207
221
|
|
208
222
|
# Log the creation
|
@@ -274,9 +288,18 @@ class SiteSyncService:
|
|
274
288
|
'last_checked': timezone.now().isoformat()
|
275
289
|
}
|
276
290
|
|
277
|
-
# Update site status
|
291
|
+
# Update site status - preserve subdomain settings
|
278
292
|
site.maintenance_active = maintenance_active
|
279
293
|
site.is_active = not zone.paused
|
294
|
+
|
295
|
+
# Ensure subdomain fields are not reset to None during status update
|
296
|
+
if site.include_subdomains is None:
|
297
|
+
site.include_subdomains = True
|
298
|
+
if site.subdomain_list is None:
|
299
|
+
site.subdomain_list = ''
|
300
|
+
if site.maintenance_url is None:
|
301
|
+
site.maintenance_url = ''
|
302
|
+
|
280
303
|
site.save()
|
281
304
|
|
282
305
|
return status_info
|
@@ -373,14 +396,36 @@ def sync_site_from_cloudflare(site: CloudflareSite) -> MaintenanceLog:
|
|
373
396
|
"""
|
374
397
|
try:
|
375
398
|
sync_service = SiteSyncService(site.api_key)
|
376
|
-
status_info = sync_service.check_site_status(site)
|
377
399
|
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
)
|
400
|
+
# Perform full zone sync to update site data from Cloudflare
|
401
|
+
sync_result = sync_service.sync_zones(force_update=True)
|
402
|
+
|
403
|
+
# Find the specific site result
|
404
|
+
site_result = None
|
405
|
+
for site_info in sync_result.get('sites', []):
|
406
|
+
if site_info.get('domain') == site.domain:
|
407
|
+
site_result = site_info
|
408
|
+
break
|
409
|
+
|
410
|
+
response_data = {
|
411
|
+
'sync_result': sync_result,
|
412
|
+
'site_result': site_result
|
413
|
+
}
|
414
|
+
|
415
|
+
if sync_result.get('errors', 0) == 0:
|
416
|
+
return MaintenanceLog.log_success(
|
417
|
+
site=site,
|
418
|
+
action=MaintenanceLog.Action.SYNC,
|
419
|
+
reason="Manual site sync - force update from Cloudflare",
|
420
|
+
cloudflare_response=response_data
|
421
|
+
)
|
422
|
+
else:
|
423
|
+
return MaintenanceLog.log_failure(
|
424
|
+
site=site,
|
425
|
+
action=MaintenanceLog.Action.SYNC,
|
426
|
+
error_message=f"Sync completed with {sync_result['errors']} errors",
|
427
|
+
cloudflare_response=response_data
|
428
|
+
)
|
384
429
|
|
385
430
|
except Exception as e:
|
386
431
|
return MaintenanceLog.log_failure(
|
django_cfg/core/config.py
CHANGED
@@ -97,7 +97,7 @@ class DjangoConfig(BaseModel):
|
|
97
97
|
|
98
98
|
model_config = {
|
99
99
|
"validate_assignment": True,
|
100
|
-
"extra": "
|
100
|
+
"extra": "forbid", # Forbid arbitrary fields for type safety
|
101
101
|
"env_prefix": "DJANGO_",
|
102
102
|
"populate_by_name": True,
|
103
103
|
"validate_default": True,
|
@@ -232,6 +232,24 @@ class DjangoConfig(BaseModel):
|
|
232
232
|
description="Force SSL redirect on/off (None = auto based on domains and environment)",
|
233
233
|
)
|
234
234
|
|
235
|
+
# === CORS Configuration ===
|
236
|
+
cors_allow_headers: List[str] = Field(
|
237
|
+
default_factory=lambda: [
|
238
|
+
"accept",
|
239
|
+
"accept-encoding",
|
240
|
+
"authorization",
|
241
|
+
"content-type",
|
242
|
+
"dnt",
|
243
|
+
"origin",
|
244
|
+
"user-agent",
|
245
|
+
"x-csrftoken",
|
246
|
+
"x-requested-with",
|
247
|
+
"x-api-key",
|
248
|
+
"x-api-token",
|
249
|
+
],
|
250
|
+
description="CORS allowed headers with common defaults for API usage",
|
251
|
+
)
|
252
|
+
|
235
253
|
# === Services Configuration ===
|
236
254
|
email: Optional[EmailConfig] = Field(
|
237
255
|
default=None,
|
@@ -733,6 +751,7 @@ class DjangoConfig(BaseModel):
|
|
733
751
|
return f"{self.site_url.rstrip('/')}/{path}"
|
734
752
|
return self.site_url
|
735
753
|
|
754
|
+
|
736
755
|
def invalidate_cache(self) -> None:
|
737
756
|
"""Invalidate cached Django settings to force regeneration."""
|
738
757
|
self._django_settings = None
|
django_cfg/core/generation.py
CHANGED
@@ -43,6 +43,7 @@ class Command(BaseCommand):
|
|
43
43
|
if options['verbose']:
|
44
44
|
self.show_database_config()
|
45
45
|
self.show_app_config()
|
46
|
+
self.show_cors_config()
|
46
47
|
|
47
48
|
if options['email_test']:
|
48
49
|
self.test_email_connection()
|
@@ -141,6 +142,59 @@ class Command(BaseCommand):
|
|
141
142
|
if not cfg_apps:
|
142
143
|
self.stdout.write(" ❌ No django_cfg apps found")
|
143
144
|
|
145
|
+
def show_cors_config(self):
|
146
|
+
"""Show CORS and security configuration."""
|
147
|
+
self.stdout.write(self.style.SUCCESS("\n🌐 CORS & Security Configuration:"))
|
148
|
+
|
149
|
+
# Get current config
|
150
|
+
try:
|
151
|
+
from django_cfg.core.config import get_current_config
|
152
|
+
config = get_current_config()
|
153
|
+
|
154
|
+
if config:
|
155
|
+
self.stdout.write(f" 📋 Security Domains: {config.security_domains}")
|
156
|
+
self.stdout.write(f" 🔗 CORS Headers: {config.cors_allow_headers}")
|
157
|
+
self.stdout.write(f" 🔒 SSL Redirect: {config.ssl_redirect}")
|
158
|
+
else:
|
159
|
+
self.stdout.write(" ⚠️ No django-cfg config instance found")
|
160
|
+
|
161
|
+
except Exception as e:
|
162
|
+
self.stdout.write(self.style.ERROR(f" ❌ Error getting config: {e}"))
|
163
|
+
|
164
|
+
# Show Django settings
|
165
|
+
cors_settings = {
|
166
|
+
'CORS_ALLOW_ALL_ORIGINS': getattr(settings, 'CORS_ALLOW_ALL_ORIGINS', None),
|
167
|
+
'CORS_ALLOWED_ORIGINS': getattr(settings, 'CORS_ALLOWED_ORIGINS', None),
|
168
|
+
'CORS_ALLOW_CREDENTIALS': getattr(settings, 'CORS_ALLOW_CREDENTIALS', None),
|
169
|
+
'CORS_ALLOW_HEADERS': getattr(settings, 'CORS_ALLOW_HEADERS', None),
|
170
|
+
'CSRF_TRUSTED_ORIGINS': getattr(settings, 'CSRF_TRUSTED_ORIGINS', None),
|
171
|
+
}
|
172
|
+
|
173
|
+
self.stdout.write("\n 🔧 Generated Django Settings:")
|
174
|
+
for key, value in cors_settings.items():
|
175
|
+
if value is not None:
|
176
|
+
icon = "✅"
|
177
|
+
if key == 'CORS_ALLOW_HEADERS' and isinstance(value, list):
|
178
|
+
# Show first few headers for readability
|
179
|
+
display_value = value[:3] + ['...'] if len(value) > 3 else value
|
180
|
+
self.stdout.write(f" {icon} {key}: {display_value} ({len(value)} total)")
|
181
|
+
elif key in ['CORS_ALLOWED_ORIGINS', 'CSRF_TRUSTED_ORIGINS'] and isinstance(value, list):
|
182
|
+
self.stdout.write(f" {icon} {key}: {value}")
|
183
|
+
else:
|
184
|
+
self.stdout.write(f" {icon} {key}: {value}")
|
185
|
+
else:
|
186
|
+
self.stdout.write(f" ⭕ {key}: Not set")
|
187
|
+
|
188
|
+
# Analysis
|
189
|
+
self.stdout.write("\n 📊 CORS Analysis:")
|
190
|
+
if cors_settings['CORS_ALLOW_ALL_ORIGINS']:
|
191
|
+
self.stdout.write(" 🟡 Development mode: All origins allowed")
|
192
|
+
elif cors_settings['CORS_ALLOWED_ORIGINS']:
|
193
|
+
origins_count = len(cors_settings['CORS_ALLOWED_ORIGINS'])
|
194
|
+
self.stdout.write(f" 🟢 Production mode: {origins_count} specific origins allowed")
|
195
|
+
else:
|
196
|
+
self.stdout.write(" 🔴 No CORS origins configured")
|
197
|
+
|
144
198
|
def test_email_connection(self):
|
145
199
|
"""Test email connection."""
|
146
200
|
self.stdout.write(self.style.SUCCESS("\n🧪 Testing Email Connection:"))
|
@@ -168,7 +168,8 @@ class SmartDefaults:
|
|
168
168
|
domains: List[str],
|
169
169
|
environment: Optional[str] = None,
|
170
170
|
debug: bool = False,
|
171
|
-
ssl_redirect: Optional[bool] = None
|
171
|
+
ssl_redirect: Optional[bool] = None,
|
172
|
+
cors_allow_headers: Optional[List[str]] = None
|
172
173
|
) -> Dict[str, Any]:
|
173
174
|
"""
|
174
175
|
Get security defaults based on environment and domains.
|
@@ -178,6 +179,7 @@ class SmartDefaults:
|
|
178
179
|
environment: Current environment
|
179
180
|
debug: Django DEBUG setting
|
180
181
|
ssl_redirect: Force SSL redirect on/off (None = auto based on domains)
|
182
|
+
cors_allow_headers: Additional CORS headers to extend defaults
|
181
183
|
|
182
184
|
Returns:
|
183
185
|
Security settings dictionary
|
@@ -185,92 +187,23 @@ class SmartDefaults:
|
|
185
187
|
try:
|
186
188
|
settings = {}
|
187
189
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
settings['CORS_ALLOW_ALL_ORIGINS'] = True
|
193
|
-
settings['CORS_ALLOW_CREDENTIALS'] = True
|
194
|
-
|
195
|
-
# For development, add ALL domains to CSRF trusted origins
|
196
|
-
# This allows testing with production domains in dev environment
|
197
|
-
csrf_trusted_origins = []
|
198
|
-
for domain in domains:
|
199
|
-
if domain.startswith(('localhost', '127.0.0.1', '0.0.0.0')):
|
200
|
-
# Local domains: add HTTP with common ports
|
201
|
-
csrf_trusted_origins.extend([
|
202
|
-
f"http://{domain}",
|
203
|
-
f"http://{domain}:8000",
|
204
|
-
f"http://{domain}:3000",
|
205
|
-
])
|
206
|
-
elif domain.endswith('.local'):
|
207
|
-
# .local domains: add both HTTP and HTTPS
|
208
|
-
csrf_trusted_origins.extend([
|
209
|
-
f"http://{domain}",
|
210
|
-
f"https://{domain}",
|
211
|
-
])
|
212
|
-
else:
|
213
|
-
# External domains: add HTTPS for dev testing with production domains
|
214
|
-
csrf_trusted_origins.extend([
|
215
|
-
f"https://{domain}",
|
216
|
-
f"http://{domain}", # Also HTTP for flexibility
|
217
|
-
])
|
218
|
-
|
219
|
-
if csrf_trusted_origins:
|
220
|
-
settings['CSRF_TRUSTED_ORIGINS'] = csrf_trusted_origins
|
221
|
-
else:
|
222
|
-
# Production: Restrict to specified domains
|
223
|
-
allowed_origins = []
|
224
|
-
csrf_trusted_origins = []
|
225
|
-
|
226
|
-
for domain in domains:
|
227
|
-
# CORS origins - only add www for root domains
|
228
|
-
allowed_origins.append(f"https://{domain}")
|
229
|
-
|
230
|
-
# Only add www. for root domains (no subdomains)
|
231
|
-
if '.' in domain and domain.count('.') == 1:
|
232
|
-
allowed_origins.append(f"https://www.{domain}")
|
233
|
-
|
234
|
-
# CSRF trusted origins (critical for HTTPS)
|
235
|
-
csrf_trusted_origins.append(f"https://{domain}")
|
236
|
-
|
237
|
-
# Only add www. for root domains (no subdomains)
|
238
|
-
if '.' in domain and domain.count('.') == 1:
|
239
|
-
csrf_trusted_origins.append(f"https://www.{domain}")
|
240
|
-
|
241
|
-
settings['CORS_ALLOWED_ORIGINS'] = allowed_origins
|
242
|
-
settings['CORS_ALLOW_CREDENTIALS'] = True
|
243
|
-
|
244
|
-
# Set CSRF trusted origins for HTTPS domains
|
245
|
-
if csrf_trusted_origins:
|
246
|
-
settings['CSRF_TRUSTED_ORIGINS'] = csrf_trusted_origins
|
190
|
+
if not domains:
|
191
|
+
return settings
|
192
|
+
|
193
|
+
is_dev = environment == "development" or debug
|
247
194
|
|
248
|
-
#
|
195
|
+
# Generate CORS settings
|
196
|
+
cors_settings = cls._generate_cors_settings(domains, is_dev, cors_allow_headers)
|
197
|
+
settings.update(cors_settings)
|
198
|
+
|
199
|
+
# Generate CSRF trusted origins
|
200
|
+
csrf_settings = cls._generate_csrf_settings(domains, is_dev)
|
201
|
+
settings.update(csrf_settings)
|
202
|
+
|
203
|
+
# Generate security headers for production
|
249
204
|
if environment == "production":
|
250
|
-
|
251
|
-
|
252
|
-
'SECURE_CONTENT_TYPE_NOSNIFF': True,
|
253
|
-
'SECURE_HSTS_SECONDS': 31536000, # 1 year
|
254
|
-
'SECURE_HSTS_INCLUDE_SUBDOMAINS': True,
|
255
|
-
'SECURE_HSTS_PRELOAD': True,
|
256
|
-
'X_FRAME_OPTIONS': 'DENY',
|
257
|
-
})
|
258
|
-
|
259
|
-
# SSL settings - configurable or auto-detect based on domains
|
260
|
-
should_use_ssl = ssl_redirect if ssl_redirect is not None else bool(domains)
|
261
|
-
if should_use_ssl:
|
262
|
-
settings.update({
|
263
|
-
'SECURE_SSL_REDIRECT': True,
|
264
|
-
'SESSION_COOKIE_SECURE': True,
|
265
|
-
'CSRF_COOKIE_SECURE': True,
|
266
|
-
})
|
267
|
-
elif ssl_redirect is False:
|
268
|
-
# Explicitly disable SSL redirect
|
269
|
-
settings.update({
|
270
|
-
'SECURE_SSL_REDIRECT': False,
|
271
|
-
'SESSION_COOKIE_SECURE': False,
|
272
|
-
'CSRF_COOKIE_SECURE': False,
|
273
|
-
})
|
205
|
+
security_headers = cls._generate_security_headers(domains, ssl_redirect)
|
206
|
+
settings.update(security_headers)
|
274
207
|
|
275
208
|
return settings
|
276
209
|
|
@@ -284,6 +217,199 @@ class SmartDefaults:
|
|
284
217
|
}
|
285
218
|
) from e
|
286
219
|
|
220
|
+
@classmethod
|
221
|
+
def _generate_cors_settings(
|
222
|
+
cls,
|
223
|
+
domains: List[str],
|
224
|
+
is_dev: bool,
|
225
|
+
cors_allow_headers: Optional[List[str]] = None
|
226
|
+
) -> Dict[str, Any]:
|
227
|
+
"""Generate CORS-specific settings."""
|
228
|
+
settings = {
|
229
|
+
'CORS_ALLOW_CREDENTIALS': True,
|
230
|
+
'CORS_ALLOW_HEADERS': cls._get_cors_headers(cors_allow_headers)
|
231
|
+
}
|
232
|
+
|
233
|
+
if is_dev:
|
234
|
+
# Development: Allow all origins for convenience
|
235
|
+
settings['CORS_ALLOW_ALL_ORIGINS'] = True
|
236
|
+
else:
|
237
|
+
# Production: Restrict to specified domains
|
238
|
+
settings['CORS_ALLOWED_ORIGINS'] = cls._build_allowed_origins(domains)
|
239
|
+
|
240
|
+
return settings
|
241
|
+
|
242
|
+
@classmethod
|
243
|
+
def _generate_csrf_settings(cls, domains: List[str], is_dev: bool) -> Dict[str, Any]:
|
244
|
+
"""Generate CSRF trusted origins."""
|
245
|
+
if is_dev:
|
246
|
+
csrf_origins = cls._build_dev_csrf_origins(domains)
|
247
|
+
else:
|
248
|
+
csrf_origins = cls._build_prod_csrf_origins(domains)
|
249
|
+
|
250
|
+
# Only return CSRF settings if we have origins to set
|
251
|
+
if csrf_origins:
|
252
|
+
return {'CSRF_TRUSTED_ORIGINS': csrf_origins}
|
253
|
+
|
254
|
+
return {}
|
255
|
+
|
256
|
+
@classmethod
|
257
|
+
def _generate_security_headers(
|
258
|
+
cls,
|
259
|
+
domains: List[str],
|
260
|
+
ssl_redirect: Optional[bool] = None
|
261
|
+
) -> Dict[str, Any]:
|
262
|
+
"""Generate security headers for production."""
|
263
|
+
settings = {
|
264
|
+
'SECURE_BROWSER_XSS_FILTER': True,
|
265
|
+
'SECURE_CONTENT_TYPE_NOSNIFF': True,
|
266
|
+
'SECURE_HSTS_SECONDS': 31536000, # 1 year
|
267
|
+
'SECURE_HSTS_INCLUDE_SUBDOMAINS': True,
|
268
|
+
'SECURE_HSTS_PRELOAD': True,
|
269
|
+
'X_FRAME_OPTIONS': 'DENY',
|
270
|
+
}
|
271
|
+
|
272
|
+
# SSL settings - configurable or auto-detect based on domains
|
273
|
+
should_use_ssl = ssl_redirect if ssl_redirect is not None else bool(domains)
|
274
|
+
|
275
|
+
if should_use_ssl:
|
276
|
+
settings.update({
|
277
|
+
'SECURE_SSL_REDIRECT': True,
|
278
|
+
'SESSION_COOKIE_SECURE': True,
|
279
|
+
'CSRF_COOKIE_SECURE': True,
|
280
|
+
})
|
281
|
+
elif ssl_redirect is False:
|
282
|
+
# Explicitly disable SSL redirect
|
283
|
+
settings.update({
|
284
|
+
'SECURE_SSL_REDIRECT': False,
|
285
|
+
'SESSION_COOKIE_SECURE': False,
|
286
|
+
'CSRF_COOKIE_SECURE': False,
|
287
|
+
})
|
288
|
+
|
289
|
+
return settings
|
290
|
+
|
291
|
+
@classmethod
|
292
|
+
def _get_cors_headers(cls, cors_allow_headers: Optional[List[str]] = None) -> List[str]:
|
293
|
+
"""
|
294
|
+
Get CORS headers with defaults extended by custom headers.
|
295
|
+
|
296
|
+
Note: The default headers are defined in DjangoConfig.cors_allow_headers.
|
297
|
+
This method should be consistent with those defaults.
|
298
|
+
"""
|
299
|
+
# Default headers - should match DjangoConfig.cors_allow_headers defaults
|
300
|
+
default_headers = [
|
301
|
+
"accept",
|
302
|
+
"accept-encoding",
|
303
|
+
"authorization",
|
304
|
+
"content-type",
|
305
|
+
"dnt",
|
306
|
+
"origin",
|
307
|
+
"user-agent",
|
308
|
+
"x-csrftoken",
|
309
|
+
"x-requested-with",
|
310
|
+
"x-api-key",
|
311
|
+
"x-api-token",
|
312
|
+
]
|
313
|
+
|
314
|
+
if not cors_allow_headers:
|
315
|
+
return default_headers
|
316
|
+
|
317
|
+
# Extend with custom headers and remove duplicates while preserving order
|
318
|
+
return cls._merge_headers(default_headers, cors_allow_headers)
|
319
|
+
|
320
|
+
@classmethod
|
321
|
+
def _merge_headers(cls, default_headers: List[str], custom_headers: List[str]) -> List[str]:
|
322
|
+
"""Merge header lists removing duplicates while preserving order."""
|
323
|
+
all_headers = default_headers + custom_headers
|
324
|
+
seen = set()
|
325
|
+
unique_headers = []
|
326
|
+
|
327
|
+
for header in all_headers:
|
328
|
+
header_lower = header.lower()
|
329
|
+
if header_lower not in seen:
|
330
|
+
seen.add(header_lower)
|
331
|
+
unique_headers.append(header)
|
332
|
+
|
333
|
+
return unique_headers
|
334
|
+
|
335
|
+
@classmethod
|
336
|
+
def _build_allowed_origins(cls, domains: List[str]) -> List[str]:
|
337
|
+
"""Build CORS allowed origins for production."""
|
338
|
+
allowed_origins = []
|
339
|
+
|
340
|
+
for domain in domains:
|
341
|
+
# Add HTTPS version
|
342
|
+
allowed_origins.append(f"https://{domain}")
|
343
|
+
|
344
|
+
# Add www. version if applicable
|
345
|
+
if cls._should_add_www_variant(domain):
|
346
|
+
allowed_origins.append(f"https://www.{domain}")
|
347
|
+
|
348
|
+
return allowed_origins
|
349
|
+
|
350
|
+
@classmethod
|
351
|
+
def _build_dev_csrf_origins(cls, domains: List[str]) -> List[str]:
|
352
|
+
"""Build CSRF trusted origins for development."""
|
353
|
+
csrf_origins = []
|
354
|
+
|
355
|
+
for domain in domains:
|
356
|
+
if domain.startswith(('localhost', '127.0.0.1', '0.0.0.0')):
|
357
|
+
# Local domains: add HTTP with common ports
|
358
|
+
csrf_origins.extend([
|
359
|
+
f"http://{domain}",
|
360
|
+
f"http://{domain}:8000",
|
361
|
+
f"http://{domain}:3000",
|
362
|
+
])
|
363
|
+
elif domain.endswith('.local'):
|
364
|
+
# .local domains: add both HTTP and HTTPS
|
365
|
+
csrf_origins.extend([
|
366
|
+
f"http://{domain}",
|
367
|
+
f"https://{domain}",
|
368
|
+
])
|
369
|
+
else:
|
370
|
+
# External domains: add both for dev flexibility
|
371
|
+
csrf_origins.extend([
|
372
|
+
f"https://{domain}",
|
373
|
+
f"http://{domain}",
|
374
|
+
])
|
375
|
+
|
376
|
+
return csrf_origins
|
377
|
+
|
378
|
+
@classmethod
|
379
|
+
def _build_prod_csrf_origins(cls, domains: List[str]) -> List[str]:
|
380
|
+
"""Build CSRF trusted origins for production."""
|
381
|
+
csrf_origins = []
|
382
|
+
|
383
|
+
for domain in domains:
|
384
|
+
# Add HTTPS version
|
385
|
+
csrf_origins.append(f"https://{domain}")
|
386
|
+
|
387
|
+
# Add www. version if applicable
|
388
|
+
if cls._should_add_www_variant(domain):
|
389
|
+
csrf_origins.append(f"https://www.{domain}")
|
390
|
+
|
391
|
+
return csrf_origins
|
392
|
+
|
393
|
+
@classmethod
|
394
|
+
def _should_add_www_variant(cls, domain: str) -> bool:
|
395
|
+
"""
|
396
|
+
Check if domain should get a www. variant added.
|
397
|
+
|
398
|
+
Simple rule: add www. variant only if domain doesn't already start with www.
|
399
|
+
Let users handle complex subdomain logic themselves by providing exact domains they want.
|
400
|
+
|
401
|
+
Examples:
|
402
|
+
- example.com -> True (add www.example.com)
|
403
|
+
- www.example.com -> False (already has www)
|
404
|
+
- api.example.com -> True (add www.api.example.com - let user decide)
|
405
|
+
- localhost -> False (no dot)
|
406
|
+
"""
|
407
|
+
if not domain or '.' not in domain:
|
408
|
+
return False
|
409
|
+
|
410
|
+
# Don't add www if domain already starts with www
|
411
|
+
return not domain.startswith('www.')
|
412
|
+
|
287
413
|
@classmethod
|
288
414
|
def get_database_defaults(
|
289
415
|
cls,
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: django-cfg
|
3
|
-
Version: 1.2.
|
3
|
+
Version: 1.2.21
|
4
4
|
Summary: 🚀 Next-gen Django configuration: type-safety, AI features, blazing-fast setup, and automated best practices — all in one.
|
5
5
|
Project-URL: Homepage, https://djangocfg.com
|
6
6
|
Project-URL: Documentation, https://docs.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=hdpxqrMF75kn_Gpzz8jqS11Cb75Q3GoMd01Q6Mg6MyU,1631
|
3
3
|
django_cfg/apps.py,sha256=k84brkeXJI7EgKZLEpTkM9YFZofKI4PzhFOn1cl9Msc,1656
|
4
4
|
django_cfg/config.py,sha256=ME-JKaVzcdmaGhuc1YTkEWoMKSaUasNf1SBlNz-NfrM,1399
|
5
5
|
django_cfg/urls.py,sha256=bpRFjMonQuk4UCUMxx4ueBX3YDNB7HXKFwEghQ3KR3o,793
|
@@ -214,7 +214,7 @@ django_cfg/apps/maintenance/admin/__init__.py,sha256=TqusqIrLX50xapmtOALAkJ0hN-9
|
|
214
214
|
django_cfg/apps/maintenance/admin/api_key_admin.py,sha256=l6LiLlw3M64tVssq1PFlmvahLYiq1VS-jHYJdZMrCus,5990
|
215
215
|
django_cfg/apps/maintenance/admin/log_admin.py,sha256=x0zAYGUXTckH2eS7wjW7bbGHSWlwqq1-jRz_UmSCoEI,4814
|
216
216
|
django_cfg/apps/maintenance/admin/scheduled_admin.py,sha256=Tbkah4TB0mLNLe4uZLMhYDULKRA6mnTiS3Dm6-2SaYA,14957
|
217
|
-
django_cfg/apps/maintenance/admin/site_admin.py,sha256=
|
217
|
+
django_cfg/apps/maintenance/admin/site_admin.py,sha256=MMiDsO7-_t4yTYtPTnpe6GZ2zCY07xMOebKKA2D59OQ,17714
|
218
218
|
django_cfg/apps/maintenance/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
219
219
|
django_cfg/apps/maintenance/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
220
220
|
django_cfg/apps/maintenance/management/commands/maintenance.py,sha256=I7ZpQEELSEC-itzK0U6G7HVrdtmpfHSN6ByTv4dc9pg,11557
|
@@ -225,17 +225,18 @@ django_cfg/apps/maintenance/managers/cloudflare_site_manager.py,sha256=Mr8_2u0wS
|
|
225
225
|
django_cfg/apps/maintenance/managers/maintenance_log_manager.py,sha256=SEbEIy6yEWDHVtQAJE1fixU1nOown6ONPbWngKv878w,4736
|
226
226
|
django_cfg/apps/maintenance/migrations/0001_initial.py,sha256=_TY5VA_f_ozqRRhAGADiuuU6Gd1GmM5JzOTWiEgVeFE,14757
|
227
227
|
django_cfg/apps/maintenance/migrations/0002_cloudflaresite_maintenance_url.py,sha256=6-xOC2hsn2_TdrtB8k7wkrYJX4l3Vn6m780glGxNBHg,566
|
228
|
+
django_cfg/apps/maintenance/migrations/0003_cloudflaresite_include_subdomains_and_more.py,sha256=uxhMhI9tEUjoZqbaOGRNM1Hu1hyYK1dI1kiVxytD2bM,820
|
228
229
|
django_cfg/apps/maintenance/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
229
230
|
django_cfg/apps/maintenance/models/__init__.py,sha256=z8FpMPCBLAp8Ose2_fPSYhrbuHSI7tAtRbADwhd_hu4,1074
|
230
231
|
django_cfg/apps/maintenance/models/cloudflare_api_key.py,sha256=B5pkZD9Gu_Z1XXek2VY-7SdeH0ZnXK5cKhxnaCWcv2k,3366
|
231
|
-
django_cfg/apps/maintenance/models/cloudflare_site.py,sha256=
|
232
|
+
django_cfg/apps/maintenance/models/cloudflare_site.py,sha256=tMeaMSk4z-_NnxYbzrkjEwMkVGkfPm4TSgaZ0pLoRc8,5708
|
232
233
|
django_cfg/apps/maintenance/models/maintenance_log.py,sha256=icqjQf37-EsjHWq8MsdgHHyuMNalH84R8f_z-BKIA2M,3922
|
233
234
|
django_cfg/apps/maintenance/models/scheduled_maintenance.py,sha256=J8-8efqZsZh2FQoW3b1MtXcNeuWWr7UkScRUL_YdbGg,10368
|
234
235
|
django_cfg/apps/maintenance/services/__init__.py,sha256=KdMiqVBVecJVzCh76j5ImRHa5qOLbZsyqY4ITmaCd0c,2584
|
235
236
|
django_cfg/apps/maintenance/services/bulk_operations_service.py,sha256=O3FCYhIuSNW7PE-86aBmA7nxPtbflye9Qv3WCgQ9s-A,15039
|
236
|
-
django_cfg/apps/maintenance/services/maintenance_service.py,sha256=
|
237
|
+
django_cfg/apps/maintenance/services/maintenance_service.py,sha256=oloMxhG2Ht6BQnMT3DNy1GQXfb1K9DqLdOeuQp42pV4,12830
|
237
238
|
django_cfg/apps/maintenance/services/scheduled_maintenance_service.py,sha256=X9xP13B7OPUcMsl8G_U5zUPmni7pO2ru8osASCiZcfE,15218
|
238
|
-
django_cfg/apps/maintenance/services/site_sync_service.py,sha256=
|
239
|
+
django_cfg/apps/maintenance/services/site_sync_service.py,sha256=ljtvGfq1eDqlegKjcbR9JeqsBGzpCONoNYBmwFzZNZk,15737
|
239
240
|
django_cfg/apps/maintenance/utils/__init__.py,sha256=L6UhtM9MoN1L26-l9C_Nyjp8-S-EB5_VvtvcX_E0isc,221
|
240
241
|
django_cfg/apps/maintenance/utils/retry_utils.py,sha256=O55jtdDf9om_QOf9rIAFt7ZEUeE5Ttq-KRLmSA9Bkps,4091
|
241
242
|
django_cfg/apps/newsletter/README.md,sha256=cnNXnAUsCVtelMNGYc4a8qJsoiVb--J2OOSh_Jw8piA,3202
|
@@ -323,16 +324,16 @@ django_cfg/cli/commands/__init__.py,sha256=EKLXDAx-QttnGmdjsmVANAfhxWplxl2V_2I0S
|
|
323
324
|
django_cfg/cli/commands/create_project.py,sha256=iuf965j8Yg7zxHcPb0GtFHEj73CYXC45ZJRmd6RbA9E,21025
|
324
325
|
django_cfg/cli/commands/info.py,sha256=o4S1xPJSHv2oEVqmH0X9RTF5f-8Wy9579yHkyd_PC3E,4923
|
325
326
|
django_cfg/core/__init__.py,sha256=eVK57qFOok9kTeHoNEMQ1BplkUOaQ7NB9kP9eQK1vg0,358
|
326
|
-
django_cfg/core/config.py,sha256=
|
327
|
+
django_cfg/core/config.py,sha256=M7rH99nDErty-dT_oHezZOT6HOX8hokjJOubGGKHYZc,28132
|
327
328
|
django_cfg/core/environment.py,sha256=MAoEPqIPsLVhSANT2Bz4nnus2wmbMW0RCOQxhQfDrDc,9106
|
328
329
|
django_cfg/core/exceptions.py,sha256=RTQEoU3PfR8lqqNNv5ayd_HY2yJLs3eioqUy8VM6AG4,10378
|
329
|
-
django_cfg/core/generation.py,sha256=
|
330
|
+
django_cfg/core/generation.py,sha256=Oa9bnEPsxwEaa0RP71zNl_z2p9NNljHnoCTeRu72X-0,25421
|
330
331
|
django_cfg/core/integration.py,sha256=5kzkZSd45ij0rfrBdeNUYnDalObqvK__XpoP31xFYKo,5224
|
331
332
|
django_cfg/core/validation.py,sha256=PhwDBTs24nuM3xqfIT3hXmN88VrhMT-Bk8Yb8FxjmPk,6590
|
332
333
|
django_cfg/management/__init__.py,sha256=NrLAhiS59hqjy-bipOC1abNuRiNm5BpKXmjN05VzKbM,28
|
333
334
|
django_cfg/management/commands/__init__.py,sha256=wc5DFEklUo-wB-6VAAmsV5UTbo5s3t936Lu61z4lojs,29
|
334
335
|
django_cfg/management/commands/auto_generate.py,sha256=N5IiWYV5claV6gq2rMPB1UMgkH9GK-2gNtbX0IDy754,14195
|
335
|
-
django_cfg/management/commands/check_settings.py,sha256=
|
336
|
+
django_cfg/management/commands/check_settings.py,sha256=JyVhL4dLnFTXkkMMpSnLoibZ8mfOMPOJ6RXAcESg-74,11509
|
336
337
|
django_cfg/management/commands/clear_constance.py,sha256=bSUhxEIKFLmXVilQGn3s9FZuldqDxDq9P9LmybZns_w,8043
|
337
338
|
django_cfg/management/commands/create_token.py,sha256=beHtUTuyFZhG97F9vSkaX-u7tieAZW-C6pntujGw1C8,11796
|
338
339
|
django_cfg/management/commands/generate.py,sha256=w0BF7IMftxNjxTxFuY8cw5pNKGW-LmTScJ8kZpxHu_8,4248
|
@@ -499,11 +500,11 @@ django_cfg/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG
|
|
499
500
|
django_cfg/templatetags/django_cfg.py,sha256=11_3YX0jAVuOd_fVcDixAxyDMG4V7A98SPqv8Lbfpxc,1308
|
500
501
|
django_cfg/utils/__init__.py,sha256=64wwXJuXytvwt8Ze_erSR2HmV07nGWJ6DV5wloRBvYE,435
|
501
502
|
django_cfg/utils/path_resolution.py,sha256=C9As6p4Q9l3VeoVkFDRPQWGrzAWf8O8UxLVkaI3ToVM,13899
|
502
|
-
django_cfg/utils/smart_defaults.py,sha256=
|
503
|
+
django_cfg/utils/smart_defaults.py,sha256=MxbUZwn_xbh48li7uLI6W4D9WCD2P2WO48dv85Fra5E,23057
|
503
504
|
django_cfg/utils/toolkit.py,sha256=Td8_iXNaftonF_xdZP4Y3uO65nuA_4_zditn5Q_Pfcw,23310
|
504
505
|
django_cfg/utils/version_check.py,sha256=jI4v3YMdQriUEeb_TvRl511sDghy6I75iKRDUaNpucs,4800
|
505
|
-
django_cfg-1.2.
|
506
|
-
django_cfg-1.2.
|
507
|
-
django_cfg-1.2.
|
508
|
-
django_cfg-1.2.
|
509
|
-
django_cfg-1.2.
|
506
|
+
django_cfg-1.2.21.dist-info/METADATA,sha256=eiujuJP0fSTNmByZnWkfR6nOvjzB2mPxZ-3BPtZNbjc,38434
|
507
|
+
django_cfg-1.2.21.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
508
|
+
django_cfg-1.2.21.dist-info/entry_points.txt,sha256=Ucmde4Z2wEzgb4AggxxZ0zaYDb9HpyE5blM3uJ0_VNg,56
|
509
|
+
django_cfg-1.2.21.dist-info/licenses/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
|
510
|
+
django_cfg-1.2.21.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|