django-cfg 1.2.19__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 CHANGED
@@ -32,7 +32,7 @@ Example:
32
32
  default_app_config = "django_cfg.apps.DjangoCfgConfig"
33
33
 
34
34
  # Version information
35
- __version__ = "1.2.19"
35
+ __version__ = "1.2.21"
36
36
  __license__ = "MIT"
37
37
 
38
38
  # Import registry for organized lazy loading
@@ -17,14 +17,10 @@ logger = logging.getLogger(__name__)
17
17
 
18
18
  @receiver(post_save, sender=User)
19
19
  def send_user_registration_email(sender, instance, created, **kwargs):
20
- """Send welcome email when new user is created."""
21
- if created:
22
- try:
23
- email_service = AuthEmailService(instance)
24
- email_service.send_welcome_email(instance.username)
25
- logger.info(f"Welcome email sent to {instance.email}")
26
- except Exception as e:
27
- logger.error(f"Failed to send welcome email to {instance.email}: {e}")
20
+ """Send welcome email when new user is created - DISABLED for manual control."""
21
+ # Welcome emails should only be sent through views, not automatically via signals
22
+ # This prevents unwanted emails during imports, admin creation, etc.
23
+ pass
28
24
 
29
25
 
30
26
  @receiver(pre_save, sender=User)
@@ -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 SiteSyncService for site synchronization
314
- from ..services import SiteSyncService
315
- sync_service = SiteSyncService(site.api_key)
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
- messages.success(
319
- request,
320
- f"Site '{site.name}' has been synchronized with Cloudflare."
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 Rule to redirect all traffic to maintenance page."""
145
+ """Create Page Rules to redirect all traffic to maintenance page (including subdomains)."""
146
146
  maintenance_url = self.site.get_maintenance_url()
147
- pattern = f"{self.site.domain}/*"
147
+ patterns = self.site.get_domain_patterns()
148
148
 
149
- logger.info(f"Creating page rule: {pattern} → {maintenance_url}")
149
+ logger.info(f"Creating page rules for {self.site.domain}: {patterns} → {maintenance_url}")
150
150
 
151
- response = self.client.page_rules.create(
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
- targets=[{
154
- "target": "url",
155
- "constraint": {
156
- "operator": "matches",
157
- "value": pattern
158
- }
159
- }],
160
- actions=[{
161
- "id": "forwarding_url",
162
- "value": {
163
- "url": maintenance_url,
164
- "status_code": 302
165
- }
166
- }],
167
- status="active"
168
- )
169
- return response.model_dump()
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 Rule with retry logic."""
174
- # Find the maintenance page rule
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
- maintenance_pattern = f"{self.site.domain}/*"
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 rule to delete: pattern={maintenance_pattern}, url={maintenance_url}")
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
- # Simple check - look for forwarding_url action with maintenance URL
193
- if (hasattr(rule, 'actions') and rule.actions and
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
- return response.model_dump()
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
- logger.warning(f"No maintenance page rule found for {self.site.domain}")
213
- return {"success": True, "message": "No page rule to delete"}
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
- return MaintenanceLog.log_success(
379
- site=site,
380
- action=MaintenanceLog.Action.SYNC,
381
- reason="Manual site sync",
382
- cloudflare_response=status_info
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": "allow", # Allow additional fields for extensibility
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
@@ -242,7 +242,8 @@ class SettingsGenerator:
242
242
  config.security_domains,
243
243
  config._environment,
244
244
  config.debug,
245
- config.ssl_redirect
245
+ config.ssl_redirect,
246
+ config.cors_allow_headers
246
247
  )
247
248
  settings.update(security_defaults)
248
249
 
@@ -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
- # CORS settings
189
- if domains:
190
- if environment == "development" or debug:
191
- # Development: Allow all origins for convenience
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
- # Security headers for production
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
- settings.update({
251
- 'SECURE_BROWSER_XSS_FILTER': True,
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.19
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=b1RNTbnScj7sTzukOJA9T2KTrXM-s0tfkIq0JPjq-ig,1631
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
@@ -9,7 +9,7 @@ django_cfg/apps/accounts/README.md,sha256=YkUYJ3iKMYTmm9ALK2PDnX75SDqZxgnkzNLCD5
9
9
  django_cfg/apps/accounts/__init__.py,sha256=osecEQhMJVP8ejhZzElNsAqA1fX-GPD3K5_yNwDk6IE,100
10
10
  django_cfg/apps/accounts/__models.py,sha256=65AomWYd78ptQ60drPbodxf0Ue310vmJQpQOPHL6V3E,10161
11
11
  django_cfg/apps/accounts/apps.py,sha256=Xd4XNpY1tw3zOqk_M9L6MR6oFdfFMYrfmrmbj0uelUs,492
12
- django_cfg/apps/accounts/signals.py,sha256=oIMM0H4FySCWDsavcVeBYppokVghfWTND6YrPpWM5oo,6351
12
+ django_cfg/apps/accounts/signals.py,sha256=30XFNOSws90QXoaV0xCCJnrP74CGZ9akCndNf9qIzfA,6220
13
13
  django_cfg/apps/accounts/urls.py,sha256=x3-0UMe8TKsuJ_GyC2hW0Un7wvnIVfuUSJ_x4HN5FWY,1334
14
14
  django_cfg/apps/accounts/admin/__init__.py,sha256=6lzAXnF9vQJm0x1lvsq0O-IWMWluIKqbQTfdF7eDOMo,439
15
15
  django_cfg/apps/accounts/admin/activity.py,sha256=u9THKq0cJhYhtdF4o_pY-Bkub2VArXOorRRq70WWl2c,3209
@@ -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=LAkUpcVWj6GUVtJP2jKA96YNIi9SCBSj49TRzPXBlR4,16216
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=YirWNZ63buvMAqHEFeC7Vc4vh7ukI5iHOdrE7XaJ5Mg,4030
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=MiHnDcTNAik-CWOJ_XWPsv14BZmUqVwQ46TUd6YrUzM,8641
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=F4k2NFz6dQp_sgyglFQUwyVItl_eC27uBXwLVt-5V7w,13712
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=Khn4-yrivGkovH06uUJ3_uen8lt3I54XMKT5lYk5l3Y,27636
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=zq6QZRyE8vS_fZVk2SbqHsCC_43DSvAUU4T1pMf8gSs,25374
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=5dtYecXD1GEKGABDmZZBNp6NbBXAbasGwltOwe2GOaA,8757
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=b6A1z7VO1NJGq0oUQXN5P97c3k_Ssgw6qUi0mK-4TlM,19786
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.19.dist-info/METADATA,sha256=yvMjdrDIOIqztqBJCz3OxOQV8DEC1xutxYmUQFMhAHI,38434
506
- django_cfg-1.2.19.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
507
- django_cfg-1.2.19.dist-info/entry_points.txt,sha256=Ucmde4Z2wEzgb4AggxxZ0zaYDb9HpyE5blM3uJ0_VNg,56
508
- django_cfg-1.2.19.dist-info/licenses/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
509
- django_cfg-1.2.19.dist-info/RECORD,,
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,,