django-cfg 1.2.20__py3-none-any.whl → 1.2.22__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/apps/newsletter/signals.py +9 -8
- django_cfg/apps/payments/__init__.py +8 -0
- django_cfg/apps/payments/apps.py +22 -0
- django_cfg/apps/payments/managers/__init__.py +22 -0
- django_cfg/apps/payments/managers/api_key_manager.py +35 -0
- django_cfg/apps/payments/managers/balance_manager.py +361 -0
- django_cfg/apps/payments/managers/currency_manager.py +32 -0
- django_cfg/apps/payments/managers/payment_manager.py +44 -0
- django_cfg/apps/payments/managers/subscription_manager.py +37 -0
- django_cfg/apps/payments/managers/tariff_manager.py +29 -0
- django_cfg/apps/payments/middleware/__init__.py +13 -0
- django_cfg/apps/payments/migrations/0001_initial.py +982 -0
- django_cfg/apps/payments/migrations/__init__.py +1 -0
- django_cfg/apps/payments/models/__init__.py +49 -0
- django_cfg/apps/payments/models/api_keys.py +96 -0
- django_cfg/apps/payments/models/balance.py +209 -0
- django_cfg/apps/payments/models/base.py +14 -0
- django_cfg/apps/payments/models/currencies.py +138 -0
- django_cfg/apps/payments/models/events.py +73 -0
- django_cfg/apps/payments/models/payments.py +301 -0
- django_cfg/apps/payments/models/subscriptions.py +270 -0
- django_cfg/apps/payments/models/tariffs.py +102 -0
- django_cfg/apps/payments/serializers/__init__.py +56 -0
- django_cfg/apps/payments/serializers/api_keys.py +51 -0
- django_cfg/apps/payments/serializers/balance.py +59 -0
- django_cfg/apps/payments/serializers/currencies.py +55 -0
- django_cfg/apps/payments/serializers/payments.py +62 -0
- django_cfg/apps/payments/serializers/subscriptions.py +71 -0
- django_cfg/apps/payments/serializers/tariffs.py +56 -0
- django_cfg/apps/payments/services/__init__.py +14 -0
- django_cfg/apps/payments/services/base.py +68 -0
- django_cfg/apps/payments/services/nowpayments.py +78 -0
- django_cfg/apps/payments/services/providers.py +77 -0
- django_cfg/apps/payments/services/redis_service.py +215 -0
- django_cfg/apps/payments/urls.py +78 -0
- django_cfg/apps/payments/views/__init__.py +62 -0
- django_cfg/apps/payments/views/api_key_views.py +164 -0
- django_cfg/apps/payments/views/balance_views.py +75 -0
- django_cfg/apps/payments/views/currency_views.py +111 -0
- django_cfg/apps/payments/views/payment_views.py +111 -0
- django_cfg/apps/payments/views/subscription_views.py +135 -0
- django_cfg/apps/payments/views/tariff_views.py +131 -0
- django_cfg/core/config.py +26 -1
- django_cfg/core/generation.py +2 -1
- django_cfg/management/commands/check_settings.py +54 -0
- django_cfg/models/revolution.py +14 -0
- django_cfg/modules/base.py +9 -0
- django_cfg/utils/smart_defaults.py +211 -85
- {django_cfg-1.2.20.dist-info → django_cfg-1.2.22.dist-info}/METADATA +1 -1
- {django_cfg-1.2.20.dist-info → django_cfg-1.2.22.dist-info}/RECORD +59 -17
- {django_cfg-1.2.20.dist-info → django_cfg-1.2.22.dist-info}/WHEEL +0 -0
- {django_cfg-1.2.20.dist-info → django_cfg-1.2.22.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.2.20.dist-info → django_cfg-1.2.22.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(
|
@@ -28,14 +28,15 @@ def newsletter_created(sender, instance, created, **kwargs):
|
|
28
28
|
def subscription_created(sender, instance, created, **kwargs):
|
29
29
|
"""Handle newsletter subscription creation."""
|
30
30
|
if created:
|
31
|
-
|
32
|
-
#
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
31
|
+
pass
|
32
|
+
# logger.info(f"New subscription: {instance.email} to newsletter {instance.newsletter.title}")
|
33
|
+
# # Add logic for welcome email to new subscribers
|
34
|
+
# try:
|
35
|
+
# from .services.email_service import NewsletterEmailService
|
36
|
+
# email_service = NewsletterEmailService()
|
37
|
+
# email_service.send_subscription_welcome_email(instance)
|
38
|
+
# except Exception as e:
|
39
|
+
# logger.error(f"Failed to send welcome email to {instance.email}: {e}")
|
39
40
|
|
40
41
|
|
41
42
|
@receiver(pre_delete, sender=NewsletterSubscription)
|
@@ -0,0 +1,22 @@
|
|
1
|
+
"""
|
2
|
+
Django app configuration for universal payments.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from django.apps import AppConfig
|
6
|
+
|
7
|
+
|
8
|
+
class PaymentsConfig(AppConfig):
|
9
|
+
"""Universal payments app configuration."""
|
10
|
+
|
11
|
+
default_auto_field = 'django.db.models.BigAutoField'
|
12
|
+
name = 'django_cfg.apps.payments'
|
13
|
+
label = 'django_cfg_payments'
|
14
|
+
verbose_name = 'Universal Payments'
|
15
|
+
|
16
|
+
def ready(self):
|
17
|
+
"""Called when the app is ready."""
|
18
|
+
# Import signals if any
|
19
|
+
try:
|
20
|
+
from . import signals # noqa
|
21
|
+
except ImportError:
|
22
|
+
pass
|
@@ -0,0 +1,22 @@
|
|
1
|
+
"""
|
2
|
+
Django model managers for universal payments.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from .payment_manager import UniversalPaymentManager
|
6
|
+
from .balance_manager import UserBalanceManager
|
7
|
+
from .subscription_manager import SubscriptionManager, EndpointGroupManager
|
8
|
+
from .tariff_manager import TariffManager, TariffEndpointGroupManager
|
9
|
+
from .api_key_manager import APIKeyManager
|
10
|
+
from .currency_manager import CurrencyManager, CurrencyNetworkManager
|
11
|
+
|
12
|
+
__all__ = [
|
13
|
+
'UniversalPaymentManager',
|
14
|
+
'UserBalanceManager',
|
15
|
+
'SubscriptionManager',
|
16
|
+
'EndpointGroupManager',
|
17
|
+
'TariffManager',
|
18
|
+
'TariffEndpointGroupManager',
|
19
|
+
'APIKeyManager',
|
20
|
+
'CurrencyManager',
|
21
|
+
'CurrencyNetworkManager',
|
22
|
+
]
|
@@ -0,0 +1,35 @@
|
|
1
|
+
"""
|
2
|
+
API key managers.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from django.db import models
|
6
|
+
from django.utils import timezone
|
7
|
+
|
8
|
+
|
9
|
+
class APIKeyManager(models.Manager):
|
10
|
+
"""Manager for APIKey model."""
|
11
|
+
|
12
|
+
def get_active_keys(self, user=None):
|
13
|
+
"""Get active API keys."""
|
14
|
+
queryset = self.filter(is_active=True)
|
15
|
+
if user:
|
16
|
+
queryset = queryset.filter(user=user)
|
17
|
+
return queryset
|
18
|
+
|
19
|
+
def get_expired_keys(self):
|
20
|
+
"""Get expired API keys."""
|
21
|
+
return self.filter(
|
22
|
+
expires_at__lte=timezone.now()
|
23
|
+
)
|
24
|
+
|
25
|
+
def get_valid_keys(self, user=None):
|
26
|
+
"""Get valid (active and not expired) API keys."""
|
27
|
+
now = timezone.now()
|
28
|
+
queryset = self.filter(
|
29
|
+
is_active=True
|
30
|
+
).filter(
|
31
|
+
models.Q(expires_at__isnull=True) | models.Q(expires_at__gt=now)
|
32
|
+
)
|
33
|
+
if user:
|
34
|
+
queryset = queryset.filter(user=user)
|
35
|
+
return queryset
|