django-cfg 1.1.61__py3-none-any.whl → 1.1.63__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/management/commands/rundramatiq.py +174 -202
- django_cfg/modules/django_tasks.py +54 -428
- django_cfg/modules/dramatiq_setup.py +16 -0
- {django_cfg-1.1.61.dist-info → django_cfg-1.1.63.dist-info}/METADATA +145 -4
- {django_cfg-1.1.61.dist-info → django_cfg-1.1.63.dist-info}/RECORD +9 -27
- django_cfg/apps/accounts/tests/__init__.py +0 -1
- django_cfg/apps/accounts/tests/test_models.py +0 -412
- django_cfg/apps/accounts/tests/test_otp_views.py +0 -143
- django_cfg/apps/accounts/tests/test_serializers.py +0 -331
- django_cfg/apps/accounts/tests/test_services.py +0 -401
- django_cfg/apps/accounts/tests/test_signals.py +0 -110
- django_cfg/apps/accounts/tests/test_views.py +0 -255
- django_cfg/apps/newsletter/tests/__init__.py +0 -1
- django_cfg/apps/newsletter/tests/run_tests.py +0 -47
- django_cfg/apps/newsletter/tests/test_email_integration.py +0 -256
- django_cfg/apps/newsletter/tests/test_email_tracking.py +0 -332
- django_cfg/apps/newsletter/tests/test_newsletter_manager.py +0 -83
- django_cfg/apps/newsletter/tests/test_newsletter_models.py +0 -157
- django_cfg/apps/support/tests/__init__.py +0 -0
- django_cfg/apps/support/tests/test_models.py +0 -106
- django_cfg/apps/tasks/@docs/CONFIGURATION.md +0 -663
- django_cfg/apps/tasks/@docs/README.md +0 -195
- django_cfg/apps/tasks/@docs/TASKS_QUEUES.md +0 -423
- django_cfg/apps/tasks/@docs/TROUBLESHOOTING.md +0 -506
- {django_cfg-1.1.61.dist-info → django_cfg-1.1.63.dist-info}/WHEEL +0 -0
- {django_cfg-1.1.61.dist-info → django_cfg-1.1.63.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.1.61.dist-info → django_cfg-1.1.63.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: django-cfg
|
3
|
-
Version: 1.1.
|
3
|
+
Version: 1.1.63
|
4
4
|
Summary: 🚀 Production-ready Django configuration framework with type-safe settings, smart automation, and modern developer experience
|
5
5
|
Project-URL: Homepage, https://github.com/markolofsen/django-cfg
|
6
6
|
Project-URL: Documentation, https://django-cfg.readthedocs.io
|
@@ -173,7 +173,7 @@ python manage.py runserver
|
|
173
173
|
- ✅ **Newsletter campaigns** with email tracking
|
174
174
|
- ✅ **Lead management** system with CRM integration
|
175
175
|
- ✅ **Multi-database routing** and connection pooling
|
176
|
-
- ✅ **Background task processing** with Dramatiq
|
176
|
+
- ✅ **Background task processing** with production-ready Dramatiq integration
|
177
177
|
- ✅ **Webhook testing** with built-in ngrok integration
|
178
178
|
- ✅ **Type-safe configuration** with full IDE support
|
179
179
|
|
@@ -413,8 +413,137 @@ Automatic dev/staging/production detection with appropriate defaults.
|
|
413
413
|
### 🌐 **Built-in Ngrok Integration**
|
414
414
|
Instant webhook testing with zero-config ngrok tunnels for development.
|
415
415
|
|
416
|
-
### 🔄 **Background Task Processing**
|
417
|
-
|
416
|
+
### 🔄 **Background Task Processing** %%PRIORITY:HIGH%%
|
417
|
+
|
418
|
+
Django-CFG includes a complete Dramatiq integration for reliable background job processing with automatic Redis configuration, worker management, and task monitoring.
|
419
|
+
|
420
|
+
#### Features
|
421
|
+
- **🚀 Zero Configuration** - Automatic Redis broker setup and queue management
|
422
|
+
- **⚡ Smart Worker Management** - Built-in `rundramatiq` command with Django settings integration
|
423
|
+
- **🔧 Type-Safe Task Definition** - Full IDE support for task creation and scheduling
|
424
|
+
- **📊 Task Monitoring** - Built-in commands for queue status and task management
|
425
|
+
- **🐳 Docker Ready** - Pre-configured Docker containers for production deployment
|
426
|
+
- **🛡️ Production Tested** - Battle-tested configuration used in production environments
|
427
|
+
|
428
|
+
#### Quick Setup
|
429
|
+
```python
|
430
|
+
from django_cfg import DjangoConfig
|
431
|
+
|
432
|
+
class MyConfig(DjangoConfig):
|
433
|
+
project_name: str = "My App"
|
434
|
+
|
435
|
+
# Dramatiq configuration (automatic)
|
436
|
+
redis_url: str = "redis://localhost:6379/2" # Separate DB for tasks
|
437
|
+
|
438
|
+
# Optional: Custom task settings
|
439
|
+
dramatiq: DramatiqConfig = DramatiqConfig(
|
440
|
+
processes=2, # Worker processes
|
441
|
+
threads=4, # Threads per process
|
442
|
+
redis_db=2, # Redis database for tasks
|
443
|
+
queues=["default", "high", "low"] # Available queues
|
444
|
+
)
|
445
|
+
|
446
|
+
config = MyConfig()
|
447
|
+
```
|
448
|
+
|
449
|
+
#### Task Definition
|
450
|
+
```python
|
451
|
+
import dramatiq
|
452
|
+
from django_cfg.modules.django_tasks import get_broker
|
453
|
+
|
454
|
+
# Tasks are automatically discovered from your apps
|
455
|
+
@dramatiq.actor(queue_name="high", max_retries=3)
|
456
|
+
def process_document(document_id: str) -> dict:
|
457
|
+
"""Process document asynchronously with full Django context."""
|
458
|
+
from myapp.models import Document
|
459
|
+
|
460
|
+
document = Document.objects.get(id=document_id)
|
461
|
+
# Your processing logic here
|
462
|
+
document.status = "processed"
|
463
|
+
document.save()
|
464
|
+
|
465
|
+
return {"status": "completed", "document_id": document_id}
|
466
|
+
|
467
|
+
# Queue the task
|
468
|
+
process_document.send(document_id="123")
|
469
|
+
```
|
470
|
+
|
471
|
+
#### Management Commands
|
472
|
+
```bash
|
473
|
+
# Start Dramatiq workers with Django settings
|
474
|
+
python manage.py rundramatiq --processes 4 --threads 8
|
475
|
+
|
476
|
+
# Monitor task queues and status
|
477
|
+
python manage.py task_status --queue high
|
478
|
+
|
479
|
+
# Clear specific queues
|
480
|
+
python manage.py task_clear --queue default
|
481
|
+
|
482
|
+
# Test task processing pipeline
|
483
|
+
python manage.py test_tasks
|
484
|
+
```
|
485
|
+
|
486
|
+
#### Docker Integration
|
487
|
+
```yaml
|
488
|
+
# docker-compose.yml (automatically generated)
|
489
|
+
services:
|
490
|
+
app-dramatiq:
|
491
|
+
build: .
|
492
|
+
command: ["python", "manage.py", "rundramatiq"]
|
493
|
+
environment:
|
494
|
+
- DRAMATIQ_PROCESSES=2
|
495
|
+
- DRAMATIQ_THREADS=4
|
496
|
+
- DRAMATIQ_QUEUES=default,high,low
|
497
|
+
depends_on:
|
498
|
+
- redis
|
499
|
+
- postgres
|
500
|
+
```
|
501
|
+
|
502
|
+
#### Production Features
|
503
|
+
- **🔄 Automatic Restart** - Workers restart on code changes in development
|
504
|
+
- **📈 Scaling** - Easy horizontal scaling with multiple worker containers
|
505
|
+
- **🛡️ Error Handling** - Built-in retry logic and dead letter queues
|
506
|
+
- **📊 Monitoring** - Integration with Django admin for task monitoring
|
507
|
+
- **⚡ Performance** - Optimized Redis configuration for high throughput
|
508
|
+
|
509
|
+
#### Real-World Example
|
510
|
+
```python
|
511
|
+
# Document processing pipeline
|
512
|
+
@dramatiq.actor(queue_name="knowledge", max_retries=2)
|
513
|
+
def process_document_async(document_id: str) -> dict:
|
514
|
+
"""Complete document processing with chunking and embeddings."""
|
515
|
+
try:
|
516
|
+
document = Document.objects.get(id=document_id)
|
517
|
+
|
518
|
+
# Step 1: Chunk document
|
519
|
+
chunks = create_document_chunks(document)
|
520
|
+
|
521
|
+
# Step 2: Generate embeddings
|
522
|
+
for chunk in chunks:
|
523
|
+
generate_embeddings.send(chunk.id)
|
524
|
+
|
525
|
+
# Step 3: Optimize database
|
526
|
+
optimize_document_embeddings.send(document_id)
|
527
|
+
|
528
|
+
return {"status": "completed", "chunks_created": len(chunks)}
|
529
|
+
|
530
|
+
except Exception as e:
|
531
|
+
logger.error(f"Document processing failed: {e}")
|
532
|
+
return {"status": "failed", "error": str(e)}
|
533
|
+
|
534
|
+
# Triggered automatically via Django signals
|
535
|
+
@receiver(post_save, sender=Document)
|
536
|
+
def queue_document_processing(sender, instance, created, **kwargs):
|
537
|
+
if created:
|
538
|
+
process_document_async.send(str(instance.id))
|
539
|
+
```
|
540
|
+
|
541
|
+
**Perfect for:**
|
542
|
+
- 📄 **Document Processing** - PDF parsing, text extraction, embeddings
|
543
|
+
- 📧 **Email Campaigns** - Bulk email sending with delivery tracking
|
544
|
+
- 🔄 **Data Synchronization** - API integrations and data imports
|
545
|
+
- 📊 **Report Generation** - Heavy computational tasks
|
546
|
+
- 🧹 **Cleanup Tasks** - Database maintenance and optimization
|
418
547
|
|
419
548
|
---
|
420
549
|
|
@@ -444,6 +573,7 @@ Django-CFG includes powerful management commands for development and operations:
|
|
444
573
|
| **`rundramatiq`** | Run Dramatiq background task workers | `python manage.py rundramatiq --processes 4` |
|
445
574
|
| **`task_status`** | Show Dramatiq task status and queues | `python manage.py task_status --queue high` |
|
446
575
|
| **`task_clear`** | Clear Dramatiq queues | `python manage.py task_clear --queue default` |
|
576
|
+
| **`test_tasks`** | Test Dramatiq task processing pipeline | `python manage.py test_tasks --document-id 123` |
|
447
577
|
| **`tree`** | Display Django project structure | `python manage.py tree --depth 3 --include-docs` |
|
448
578
|
| **`validate_config`** | Deep validation of all settings | `python manage.py validate_config --strict` |
|
449
579
|
|
@@ -955,6 +1085,17 @@ class ProductionConfig(DjangoConfig):
|
|
955
1085
|
enable_newsletter: bool = True # Email marketing, campaigns, tracking & analytics
|
956
1086
|
enable_leads: bool = True # Lead capture, CRM integration, source tracking
|
957
1087
|
|
1088
|
+
# === Background Task Processing ===
|
1089
|
+
redis_url: str = "redis://redis:6379/2" # Separate DB for Dramatiq tasks
|
1090
|
+
dramatiq: DramatiqConfig = DramatiqConfig(
|
1091
|
+
processes=4, # Production worker processes
|
1092
|
+
threads=8, # Threads per process for I/O bound tasks
|
1093
|
+
redis_db=2, # Dedicated Redis DB for task queues
|
1094
|
+
queues=["default", "high", "low", "knowledge", "email"], # Production queues
|
1095
|
+
max_retries=3, # Default retry attempts
|
1096
|
+
max_age=3600000, # 1 hour max task age (milliseconds)
|
1097
|
+
)
|
1098
|
+
|
958
1099
|
# === Multi-Zone API ===
|
959
1100
|
revolution: RevolutionConfig = RevolutionConfig(
|
960
1101
|
api_prefix="api/v2",
|
@@ -1,4 +1,4 @@
|
|
1
|
-
django_cfg/__init__.py,sha256=
|
1
|
+
django_cfg/__init__.py,sha256=L7If-rI58bv1bXS1v_0LJYjhiBMJIcXlZQdscCSVMrs,14288
|
2
2
|
django_cfg/apps.py,sha256=k84brkeXJI7EgKZLEpTkM9YFZofKI4PzhFOn1cl9Msc,1656
|
3
3
|
django_cfg/exceptions.py,sha256=RTQEoU3PfR8lqqNNv5ayd_HY2yJLs3eioqUy8VM6AG4,10378
|
4
4
|
django_cfg/integration.py,sha256=-7hvd-4ohLdzH4eufCZTOe3yTzPoQyB_HCfvsSm9AAw,5218
|
@@ -42,13 +42,6 @@ django_cfg/apps/accounts/templates/emails/otp_email.html,sha256=HmpPNGBxE6JnwqrQ
|
|
42
42
|
django_cfg/apps/accounts/templates/emails/otp_email.txt,sha256=5IzRhLyGuspK5H1WYtyPPV1-gF4T9fT-JZIkR8-uP0U,502
|
43
43
|
django_cfg/apps/accounts/templates/emails/welcome_email.html,sha256=X4pcPUgnDHZoWramfLjqgHHvs5RXDffueL9N8KjYLsQ,4067
|
44
44
|
django_cfg/apps/accounts/templates/emails/welcome_email.txt,sha256=jVIr4O9_jOOSU8pEZI_1UOCeJRsDOh9iAfi0qN7wR-I,748
|
45
|
-
django_cfg/apps/accounts/tests/__init__.py,sha256=U4MgTYmvZrIqeRj-RnIWQPferwuz107-38kHhNgwDdk,25
|
46
|
-
django_cfg/apps/accounts/tests/test_models.py,sha256=9rpYMWExZ3budfnkDIZRW2rkKsR_VYlJdFYooesuXig,14596
|
47
|
-
django_cfg/apps/accounts/tests/test_otp_views.py,sha256=diRf7phOtijBygg5sF0MIe9Ynf29haW6rUU7qqTge2Y,5972
|
48
|
-
django_cfg/apps/accounts/tests/test_serializers.py,sha256=kKhqzEDNkup9pfFEyl_QrM-cbJBrwj19nbTGpJiux5o,13259
|
49
|
-
django_cfg/apps/accounts/tests/test_services.py,sha256=T3tKXUAEVZaIjq3tK9Z-SRBoyiirckNzHAmEiTahKUc,15329
|
50
|
-
django_cfg/apps/accounts/tests/test_signals.py,sha256=JmpeGdRt64irGrUBocOWKlM5-ZQSeaRx1HXPHq15d_o,4398
|
51
|
-
django_cfg/apps/accounts/tests/test_views.py,sha256=J18245zx2F-L2Q7tvFjces8S_ypjUTKcH8Qj8eDaYhM,10113
|
52
45
|
django_cfg/apps/accounts/utils/notifications.py,sha256=G_EOQe6JFqYvPVooFXJUcF5YvLx8Lnd_GMLfFwKTLiI,24914
|
53
46
|
django_cfg/apps/accounts/views/__init__.py,sha256=mQRa06_tfIkemXL2MMa05Yj2w1G3pbu2VjC7h4ZE1TI,239
|
54
47
|
django_cfg/apps/accounts/views/otp.py,sha256=wPRu4wMwf5qqtTPJLsLK-QGb34ZD67ZRFJpiKjj0TY4,6618
|
@@ -90,12 +83,6 @@ django_cfg/apps/newsletter/managers/__init__.py,sha256=rJc4KSQbe-Tvp2CX3j8-5n-FB
|
|
90
83
|
django_cfg/apps/newsletter/migrations/0001_initial.py,sha256=hXv5zMz3bu6RUyS8ToFGjoDc7dIpM9q1KthzwzA8iRY,6423
|
91
84
|
django_cfg/apps/newsletter/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
92
85
|
django_cfg/apps/newsletter/services/email_service.py,sha256=GbGae6bnuBrX0KFW_2gTCdnVrf254hyNvrUvkHdrQvY,15241
|
93
|
-
django_cfg/apps/newsletter/tests/__init__.py,sha256=sEKmEHtTUiORyZGhxPTy-h2NfuKib58vwn7IWn7yVzA,16
|
94
|
-
django_cfg/apps/newsletter/tests/run_tests.py,sha256=jh8u9IG0x-VKda8_jZEKF00UODw77GufVlqG68O4yMg,1171
|
95
|
-
django_cfg/apps/newsletter/tests/test_email_integration.py,sha256=UOx9WJydzbOX7ZjYZCCRyfhO_BVrWNt64oc7LQoclEg,10199
|
96
|
-
django_cfg/apps/newsletter/tests/test_email_tracking.py,sha256=dvXmHJh5ldEXkt-Rwxe3vX_kseZ9hq3-7m8W4ORr7pI,12518
|
97
|
-
django_cfg/apps/newsletter/tests/test_newsletter_manager.py,sha256=8CYtpM_g98p-OqDQQPwTz_XzC2Oc-hqAyg3eHTvWlMQ,2893
|
98
|
-
django_cfg/apps/newsletter/tests/test_newsletter_models.py,sha256=-k8wzk7-c0efGZZXX2SQpC2Xy1A5cyyAAP2ap0gq9t4,5601
|
99
86
|
django_cfg/apps/newsletter/utils/__init__.py,sha256=rrMmQU1Jp3xLDX7EOOLb4nX8I83UzDifRNxDRlIyqfA,27
|
100
87
|
django_cfg/apps/newsletter/views/__init__.py,sha256=zsM1d9dL1RFtPZUGg6n7dQywGwbhT1R9MSwHOQFJEwo,979
|
101
88
|
django_cfg/apps/newsletter/views/campaigns.py,sha256=Z7b0Sjjx80vGcyjbY2ByFRSA0VezofK1_WYkX2MCbZ0,4673
|
@@ -118,8 +105,6 @@ django_cfg/apps/support/migrations/0002_alter_message_ticket.py,sha256=MxdjTW7hJ
|
|
118
105
|
django_cfg/apps/support/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
119
106
|
django_cfg/apps/support/templates/support/chat/access_denied.html,sha256=QdVXR-61aUHmh9Vlag_zXGcS_BG7a807LesIbduqnag,1094
|
120
107
|
django_cfg/apps/support/templates/support/chat/ticket_chat.html,sha256=b2UvCQp122-CRl567J_sLCvcp_zZi21VG4DOy7dmjM8,14471
|
121
|
-
django_cfg/apps/support/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
122
|
-
django_cfg/apps/support/tests/test_models.py,sha256=sX4_ArzKEZRDVrmCbtGgk28RDnmWAm6KihLBbnBIchY,4620
|
123
108
|
django_cfg/apps/support/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
124
109
|
django_cfg/apps/support/utils/support_email_service.py,sha256=0l4dXqAG6oN2UR7W3JzuRhlomaWa41IT7ceVcanluKs,5570
|
125
110
|
django_cfg/apps/support/views/__init__.py,sha256=E-Iqiy888SncfBTiSyXyGCjaNrMa5Klvapzb3ItYPXc,448
|
@@ -132,10 +117,6 @@ django_cfg/apps/tasks/apps.py,sha256=WXQvQjFVBuiO7s1ikd4Sf15YKRTvruVzMTo82uNXY08
|
|
132
117
|
django_cfg/apps/tasks/serializers.py,sha256=o323j6bmeNAVCu-FA3Jrve2tLuFOHMgkfXgZUdsTuS8,2751
|
133
118
|
django_cfg/apps/tasks/urls.py,sha256=NwEk6Vjq_ETEzzbx6R-9Qv3EbCxmX31xfzLICrlQ6cw,587
|
134
119
|
django_cfg/apps/tasks/views.py,sha256=HO4fSHyYALOKmLiLsm2Ir-G2DgCsZGHuoncq8FVRp5s,18048
|
135
|
-
django_cfg/apps/tasks/@docs/CONFIGURATION.md,sha256=erW3GJG5WBoiZn8dzcj6yH9o6zNrO8_kkX7srleXjeY,16357
|
136
|
-
django_cfg/apps/tasks/@docs/README.md,sha256=A1A4e56ed1Dh6R5XHEgInOkHv25fUQfJGVIs9f_G1ZM,6363
|
137
|
-
django_cfg/apps/tasks/@docs/TASKS_QUEUES.md,sha256=5AzkyDFZg3YJ9UVfRNNdO9_z6_7t6c6CScwz0yCWsTc,10233
|
138
|
-
django_cfg/apps/tasks/@docs/TROUBLESHOOTING.md,sha256=B81bP8RVSc4U3j9hZp-Aq40rgzHYQQvnXU1_DnueKtQ,11473
|
139
120
|
django_cfg/apps/tasks/static/tasks/css/dashboard.css,sha256=mDtAwFWzNrWEJkbkQFewmzFSYRXgTfGIXVkfWkbcUW0,4765
|
140
121
|
django_cfg/apps/tasks/static/tasks/js/api.js,sha256=01OL5P0AO4fg9vCjVNx5s1mGgh6Ate69rucfA27CS3c,4048
|
141
122
|
django_cfg/apps/tasks/static/tasks/js/dashboard.js,sha256=iBizGudFpzkX29cJbYv7Zim2xHx94uoBtmPCJ7t6TzE,23496
|
@@ -175,7 +156,7 @@ django_cfg/management/commands/create_token.py,sha256=beHtUTuyFZhG97F9vSkaX-u7ti
|
|
175
156
|
django_cfg/management/commands/generate.py,sha256=w0BF7IMftxNjxTxFuY8cw5pNKGW-LmTScJ8kZpxHu_8,4248
|
176
157
|
django_cfg/management/commands/list_urls.py,sha256=D8ikInA3uE1LbQGLWmfdLnEqPg7wqrI3caQA6iTe_-0,11009
|
177
158
|
django_cfg/management/commands/migrator.py,sha256=mhMM63uv_Jp9zHVVM7TMwCB90uv3iFZn1vOG-rXyi3s,16191
|
178
|
-
django_cfg/management/commands/rundramatiq.py,sha256=
|
159
|
+
django_cfg/management/commands/rundramatiq.py,sha256=1e07PWrtSnxTPZ8RCpgY_yxwV2y-EAduVZDZ34DRqoI,8896
|
179
160
|
django_cfg/management/commands/runserver_ngrok.py,sha256=mcTioDIzHgha6sGo5eazlJhdKr8y5-uEQIc3qG3AvCI,5237
|
180
161
|
django_cfg/management/commands/script.py,sha256=I6zOEQEGaED0HoLxl2EqKz39HwbKg9HhdxnGKybfH5s,16974
|
181
162
|
django_cfg/management/commands/show_config.py,sha256=0YJ99P1XvymT3fWritaNmn_HJ-PVb0I-yBy757M_bn8,8337
|
@@ -208,8 +189,9 @@ django_cfg/modules/base.py,sha256=X90X-0iBfnaUSaC7S8_ULa_rdT41tqTVJnT05_RuyK4,47
|
|
208
189
|
django_cfg/modules/django_email.py,sha256=uBvvqRVe1DG73Qq2d2IBYTjhFRdvHgsIbkVw3ge9OW8,16586
|
209
190
|
django_cfg/modules/django_logger.py,sha256=VfcPCurTdU3iI593EJNs3wUoWQowu7-ykJGuHNkE79M,6325
|
210
191
|
django_cfg/modules/django_ngrok.py,sha256=OAvir2pBFHfko-XaVgZTjeJwyZw-NSEILaKNlqQziqA,10476
|
211
|
-
django_cfg/modules/django_tasks.py,sha256=
|
192
|
+
django_cfg/modules/django_tasks.py,sha256=Vo45h4x8UHieAYdjp3g79efMns0cotkwp5ko4Fbt_pI,12868
|
212
193
|
django_cfg/modules/django_telegram.py,sha256=Mun2tAm0P2cUyQlAs8FaPe-FVgcrv7L_-FPTXQQEUT0,16356
|
194
|
+
django_cfg/modules/dramatiq_setup.py,sha256=Jke4aO_L1t6F3OAc4pl12zppKzL0gb1p6ilfQ3zUIZ8,454
|
213
195
|
django_cfg/modules/logger.py,sha256=4_zeasNehr8LGz8r_ckv15-fQS63zCodiqD4CYIEyFI,10546
|
214
196
|
django_cfg/modules/django_currency/README.md,sha256=Ox3jgRtsbOIaMuYDkIhrs9ijLGLbn-2R7mD9n2tjAVE,8512
|
215
197
|
django_cfg/modules/django_currency/__init__.py,sha256=SLzzYkkqoz9EsspkzEK0yZ4_Q3JKmb3e_c1GfdYF3GY,1294
|
@@ -270,8 +252,8 @@ django_cfg/templates/emails/base_email.html,sha256=TWcvYa2IHShlF_E8jf1bWZStRO0v8
|
|
270
252
|
django_cfg/utils/__init__.py,sha256=64wwXJuXytvwt8Ze_erSR2HmV07nGWJ6DV5wloRBvYE,435
|
271
253
|
django_cfg/utils/path_resolution.py,sha256=eML-6-RIGTs5TePktIQN8nxfDUEFJ3JA0AzWBcihAbs,13894
|
272
254
|
django_cfg/utils/smart_defaults.py,sha256=iL6_n3jGDW5812whylWAwXik0xBSYihdLp4IJ26T5eA,20547
|
273
|
-
django_cfg-1.1.
|
274
|
-
django_cfg-1.1.
|
275
|
-
django_cfg-1.1.
|
276
|
-
django_cfg-1.1.
|
277
|
-
django_cfg-1.1.
|
255
|
+
django_cfg-1.1.63.dist-info/METADATA,sha256=cJwYgC8qkHiiP8nD8yAfE6ps9-SQyYQA9ItJCttPjmo,44084
|
256
|
+
django_cfg-1.1.63.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
257
|
+
django_cfg-1.1.63.dist-info/entry_points.txt,sha256=Ucmde4Z2wEzgb4AggxxZ0zaYDb9HpyE5blM3uJ0_VNg,56
|
258
|
+
django_cfg-1.1.63.dist-info/licenses/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
|
259
|
+
django_cfg-1.1.63.dist-info/RECORD,,
|
@@ -1 +0,0 @@
|
|
1
|
-
# Tests for accounts app
|
@@ -1,412 +0,0 @@
|
|
1
|
-
from django.test import TestCase
|
2
|
-
from django.contrib.auth import get_user_model
|
3
|
-
from django.utils import timezone
|
4
|
-
from datetime import timedelta
|
5
|
-
|
6
|
-
from ..models import OTPSecret, RegistrationSource, UserRegistrationSource
|
7
|
-
|
8
|
-
User = get_user_model()
|
9
|
-
|
10
|
-
|
11
|
-
class CustomUserModelTest(TestCase):
|
12
|
-
"""Test CustomUser model."""
|
13
|
-
|
14
|
-
def setUp(self):
|
15
|
-
self.user_data = {
|
16
|
-
'email': 'test@example.com',
|
17
|
-
'username': 'testuser',
|
18
|
-
'password': 'testpass123'
|
19
|
-
}
|
20
|
-
|
21
|
-
def test_create_user(self):
|
22
|
-
"""Test creating a user."""
|
23
|
-
user = User.objects.create_user(**self.user_data)
|
24
|
-
|
25
|
-
self.assertEqual(user.email, 'test@example.com')
|
26
|
-
self.assertEqual(user.username, 'testuser')
|
27
|
-
self.assertTrue(user.check_password('testpass123'))
|
28
|
-
self.assertTrue(user.is_active)
|
29
|
-
self.assertFalse(user.is_staff)
|
30
|
-
|
31
|
-
def test_create_superuser(self):
|
32
|
-
"""Test creating a superuser."""
|
33
|
-
user = User.objects.create_superuser(**self.user_data)
|
34
|
-
|
35
|
-
self.assertTrue(user.is_staff)
|
36
|
-
self.assertTrue(user.is_superuser)
|
37
|
-
self.assertTrue(user.is_admin)
|
38
|
-
|
39
|
-
def test_user_string_representation(self):
|
40
|
-
"""Test user string representation."""
|
41
|
-
user = User.objects.create_user(**self.user_data)
|
42
|
-
self.assertEqual(str(user), 'test@example.com')
|
43
|
-
|
44
|
-
def test_email_unique(self):
|
45
|
-
"""Test email uniqueness."""
|
46
|
-
User.objects.create_user(**self.user_data)
|
47
|
-
|
48
|
-
with self.assertRaises(Exception):
|
49
|
-
User.objects.create_user(**self.user_data)
|
50
|
-
|
51
|
-
def test_user_creation(self):
|
52
|
-
"""Test user creation."""
|
53
|
-
user_data = {
|
54
|
-
'email': 'test@example.com',
|
55
|
-
'first_name': 'Test',
|
56
|
-
'last_name': 'User',
|
57
|
-
}
|
58
|
-
user = User.objects.create_user(**user_data)
|
59
|
-
|
60
|
-
self.assertEqual(user.email, 'test@example.com')
|
61
|
-
self.assertEqual(user.first_name, 'Test')
|
62
|
-
self.assertEqual(user.last_name, 'User')
|
63
|
-
self.assertTrue(user.is_active)
|
64
|
-
self.assertFalse(user.is_staff)
|
65
|
-
self.assertFalse(user.is_superuser)
|
66
|
-
self.assertFalse(user.phone_verified) # New field default
|
67
|
-
|
68
|
-
def test_user_phone_verification(self):
|
69
|
-
"""Test user phone verification field."""
|
70
|
-
user = User.objects.create_user(
|
71
|
-
email='test@example.com',
|
72
|
-
phone='+1234567890'
|
73
|
-
)
|
74
|
-
|
75
|
-
# Initially phone not verified
|
76
|
-
self.assertFalse(user.phone_verified)
|
77
|
-
|
78
|
-
# Mark phone as verified
|
79
|
-
user.phone_verified = True
|
80
|
-
user.save()
|
81
|
-
|
82
|
-
user.refresh_from_db()
|
83
|
-
self.assertTrue(user.phone_verified)
|
84
|
-
|
85
|
-
def test_user_get_identifier_for_otp(self):
|
86
|
-
"""Test getting identifier for OTP based on channel."""
|
87
|
-
user = User.objects.create_user(
|
88
|
-
email='test@example.com',
|
89
|
-
phone='+1234567890'
|
90
|
-
)
|
91
|
-
|
92
|
-
# Test email channel
|
93
|
-
self.assertEqual(user.get_identifier_for_otp('email'), 'test@example.com')
|
94
|
-
|
95
|
-
# Test phone channel
|
96
|
-
self.assertEqual(user.get_identifier_for_otp('phone'), '+1234567890')
|
97
|
-
|
98
|
-
# Test default (email)
|
99
|
-
self.assertEqual(user.get_identifier_for_otp(), 'test@example.com')
|
100
|
-
|
101
|
-
|
102
|
-
class OTPSecretModelTest(TestCase):
|
103
|
-
"""Test OTPSecret model."""
|
104
|
-
|
105
|
-
def setUp(self):
|
106
|
-
self.email = 'test@example.com'
|
107
|
-
|
108
|
-
def test_otp_generation(self):
|
109
|
-
"""Test OTP generation."""
|
110
|
-
otp = OTPSecret.generate_otp()
|
111
|
-
self.assertEqual(len(otp), 6)
|
112
|
-
self.assertTrue(otp.isdigit())
|
113
|
-
|
114
|
-
def test_otp_creation(self):
|
115
|
-
"""Test OTP creation."""
|
116
|
-
otp = OTPSecret.objects.create(
|
117
|
-
email=self.email,
|
118
|
-
secret='123456'
|
119
|
-
)
|
120
|
-
self.assertEqual(otp.email, self.email)
|
121
|
-
self.assertEqual(otp.secret, '123456')
|
122
|
-
self.assertFalse(otp.is_used)
|
123
|
-
self.assertIsNotNone(otp.expires_at)
|
124
|
-
|
125
|
-
def test_otp_expiration(self):
|
126
|
-
"""Test OTP expiration."""
|
127
|
-
# Create OTP with past expiration
|
128
|
-
past_time = timezone.now() - timedelta(minutes=1)
|
129
|
-
otp = OTPSecret.objects.create(
|
130
|
-
email=self.email,
|
131
|
-
secret='123456',
|
132
|
-
expires_at=past_time
|
133
|
-
)
|
134
|
-
self.assertFalse(otp.is_valid)
|
135
|
-
|
136
|
-
def test_otp_mark_used(self):
|
137
|
-
"""Test marking OTP as used."""
|
138
|
-
otp = OTPSecret.objects.create(
|
139
|
-
email=self.email,
|
140
|
-
secret='123456'
|
141
|
-
)
|
142
|
-
self.assertFalse(otp.is_used)
|
143
|
-
otp.mark_used()
|
144
|
-
self.assertTrue(otp.is_used)
|
145
|
-
|
146
|
-
def test_otp_str_representation(self):
|
147
|
-
"""Test OTP string representation."""
|
148
|
-
otp = OTPSecret.objects.create(
|
149
|
-
email=self.email,
|
150
|
-
secret='123456'
|
151
|
-
)
|
152
|
-
self.assertEqual(str(otp), f"OTP for {self.email} (email)")
|
153
|
-
|
154
|
-
def test_otp_create_for_email(self):
|
155
|
-
"""Test creating OTP for email channel."""
|
156
|
-
otp = OTPSecret.create_for_email(self.email)
|
157
|
-
|
158
|
-
self.assertEqual(otp.channel_type, 'email')
|
159
|
-
self.assertEqual(otp.recipient, self.email)
|
160
|
-
self.assertEqual(otp.email, self.email) # Backward compatibility
|
161
|
-
self.assertEqual(len(otp.secret), 6)
|
162
|
-
self.assertTrue(otp.secret.isdigit())
|
163
|
-
self.assertFalse(otp.is_used)
|
164
|
-
|
165
|
-
def test_otp_create_for_phone(self):
|
166
|
-
"""Test creating OTP for phone channel."""
|
167
|
-
phone = '+1234567890'
|
168
|
-
otp = OTPSecret.create_for_phone(phone)
|
169
|
-
|
170
|
-
self.assertEqual(otp.channel_type, 'phone')
|
171
|
-
self.assertEqual(otp.recipient, phone)
|
172
|
-
self.assertIsNone(otp.email) # No email for phone OTP
|
173
|
-
self.assertEqual(len(otp.secret), 6)
|
174
|
-
self.assertTrue(otp.secret.isdigit())
|
175
|
-
self.assertFalse(otp.is_used)
|
176
|
-
|
177
|
-
def test_otp_channel_types(self):
|
178
|
-
"""Test different channel types."""
|
179
|
-
email_otp = OTPSecret.create_for_email('test@example.com')
|
180
|
-
phone_otp = OTPSecret.create_for_phone('+1234567890')
|
181
|
-
|
182
|
-
self.assertEqual(email_otp.channel_type, 'email')
|
183
|
-
self.assertEqual(phone_otp.channel_type, 'phone')
|
184
|
-
|
185
|
-
# Test string representations
|
186
|
-
self.assertEqual(str(email_otp), "OTP for test@example.com (email)")
|
187
|
-
self.assertEqual(str(phone_otp), "OTP for +1234567890 (phone)")
|
188
|
-
|
189
|
-
|
190
|
-
class RegistrationSourceModelTest(TestCase):
|
191
|
-
"""Test RegistrationSource model."""
|
192
|
-
|
193
|
-
def setUp(self):
|
194
|
-
self.source = RegistrationSource.objects.create(
|
195
|
-
url='https://reforms.ai',
|
196
|
-
name='Unreal Dashboard',
|
197
|
-
description='Main dashboard for Unreal project',
|
198
|
-
is_active=True
|
199
|
-
)
|
200
|
-
|
201
|
-
def test_source_creation(self):
|
202
|
-
"""Test source creation."""
|
203
|
-
self.assertEqual(self.source.url, 'https://reforms.ai')
|
204
|
-
self.assertEqual(self.source.name, 'Unreal Dashboard')
|
205
|
-
self.assertEqual(self.source.description, 'Main dashboard for Unreal project')
|
206
|
-
self.assertTrue(self.source.is_active)
|
207
|
-
self.assertIsNotNone(self.source.created_at)
|
208
|
-
self.assertIsNotNone(self.source.updated_at)
|
209
|
-
|
210
|
-
def test_source_get_domain(self):
|
211
|
-
"""Test domain extraction from URL."""
|
212
|
-
test_cases = [
|
213
|
-
('https://test1.unrealon.com', 'test1.unrealon.com'),
|
214
|
-
('https://www.example.com', 'www.example.com'),
|
215
|
-
('http://app.test.com', 'app.test.com'),
|
216
|
-
('https://sub.domain.co.uk', 'sub.domain.co.uk'),
|
217
|
-
]
|
218
|
-
|
219
|
-
for url, expected_domain in test_cases:
|
220
|
-
source = RegistrationSource.objects.create(url=url)
|
221
|
-
self.assertEqual(source.get_domain(), expected_domain)
|
222
|
-
|
223
|
-
def test_source_get_display_name(self):
|
224
|
-
"""Test display name generation."""
|
225
|
-
# Source with custom name
|
226
|
-
source_with_name = RegistrationSource.objects.create(
|
227
|
-
url='https://example.com',
|
228
|
-
name='Custom Name'
|
229
|
-
)
|
230
|
-
self.assertEqual(source_with_name.get_display_name(), 'Custom Name')
|
231
|
-
|
232
|
-
# Source without name (uses domain)
|
233
|
-
source_without_name = RegistrationSource.objects.create(
|
234
|
-
url='https://app.example.com'
|
235
|
-
)
|
236
|
-
self.assertEqual(source_without_name.get_display_name(), 'app.example.com')
|
237
|
-
|
238
|
-
def test_source_str_representation(self):
|
239
|
-
"""Test source string representation."""
|
240
|
-
# Source with custom name
|
241
|
-
source_with_name = RegistrationSource.objects.create(
|
242
|
-
url='https://example.com',
|
243
|
-
name='Custom Name'
|
244
|
-
)
|
245
|
-
self.assertEqual(str(source_with_name), 'Custom Name')
|
246
|
-
|
247
|
-
# Source without name (uses domain)
|
248
|
-
source_without_name = RegistrationSource.objects.create(
|
249
|
-
url='https://app.example.com'
|
250
|
-
)
|
251
|
-
self.assertEqual(str(source_without_name), 'app.example.com')
|
252
|
-
|
253
|
-
def test_source_unique_url(self):
|
254
|
-
"""Test that source URL must be unique."""
|
255
|
-
RegistrationSource.objects.create(url='https://example.com')
|
256
|
-
|
257
|
-
# Try to create another source with same URL
|
258
|
-
with self.assertRaises(Exception): # Should raise IntegrityError
|
259
|
-
RegistrationSource.objects.create(url='https://example.com')
|
260
|
-
|
261
|
-
def test_source_ordering(self):
|
262
|
-
"""Test source ordering by created_at descending."""
|
263
|
-
source1 = RegistrationSource.objects.create(url='https://example1.com')
|
264
|
-
source2 = RegistrationSource.objects.create(url='https://example2.com')
|
265
|
-
|
266
|
-
sources = RegistrationSource.objects.all()
|
267
|
-
self.assertEqual(sources[0], source2) # Most recent first
|
268
|
-
self.assertEqual(sources[1], source1)
|
269
|
-
|
270
|
-
|
271
|
-
class UserRegistrationSourceModelTest(TestCase):
|
272
|
-
"""Test UserRegistrationSource model."""
|
273
|
-
|
274
|
-
def setUp(self):
|
275
|
-
self.user = User.objects.create(
|
276
|
-
email='test@example.com',
|
277
|
-
username='testuser'
|
278
|
-
)
|
279
|
-
self.source = RegistrationSource.objects.create(
|
280
|
-
url='https://reforms.ai',
|
281
|
-
name='Unreal Dashboard'
|
282
|
-
)
|
283
|
-
|
284
|
-
def test_user_source_creation(self):
|
285
|
-
"""Test user-source relationship creation."""
|
286
|
-
user_source = UserRegistrationSource.objects.create(
|
287
|
-
user=self.user,
|
288
|
-
source=self.source,
|
289
|
-
first_registration=True
|
290
|
-
)
|
291
|
-
|
292
|
-
self.assertEqual(user_source.user, self.user)
|
293
|
-
self.assertEqual(user_source.source, self.source)
|
294
|
-
self.assertTrue(user_source.first_registration)
|
295
|
-
self.assertIsNotNone(user_source.registration_date)
|
296
|
-
|
297
|
-
def test_user_source_unique_constraint(self):
|
298
|
-
"""Test that user-source relationship is unique."""
|
299
|
-
UserRegistrationSource.objects.create(
|
300
|
-
user=self.user,
|
301
|
-
source=self.source,
|
302
|
-
first_registration=True
|
303
|
-
)
|
304
|
-
|
305
|
-
# Try to create duplicate relationship
|
306
|
-
with self.assertRaises(Exception): # Should raise IntegrityError
|
307
|
-
UserRegistrationSource.objects.create(
|
308
|
-
user=self.user,
|
309
|
-
source=self.source,
|
310
|
-
first_registration=False
|
311
|
-
)
|
312
|
-
|
313
|
-
def test_user_source_ordering(self):
|
314
|
-
"""Test user-source ordering by registration_date descending."""
|
315
|
-
user_source1 = UserRegistrationSource.objects.create(
|
316
|
-
user=self.user,
|
317
|
-
source=self.source,
|
318
|
-
first_registration=True
|
319
|
-
)
|
320
|
-
|
321
|
-
# Create another source and relationship
|
322
|
-
source2 = RegistrationSource.objects.create(url='https://example.com')
|
323
|
-
user_source2 = UserRegistrationSource.objects.create(
|
324
|
-
user=self.user,
|
325
|
-
source=source2,
|
326
|
-
first_registration=False
|
327
|
-
)
|
328
|
-
|
329
|
-
user_sources = UserRegistrationSource.objects.all()
|
330
|
-
self.assertEqual(user_sources[0], user_source2) # Most recent first
|
331
|
-
self.assertEqual(user_sources[1], user_source1)
|
332
|
-
|
333
|
-
|
334
|
-
class CustomUserSourceMethodsTest(TestCase):
|
335
|
-
"""Test CustomUser source-related methods."""
|
336
|
-
|
337
|
-
def setUp(self):
|
338
|
-
self.user = User.objects.create(
|
339
|
-
email='test@example.com',
|
340
|
-
username='testuser'
|
341
|
-
)
|
342
|
-
self.source1 = RegistrationSource.objects.create(
|
343
|
-
url='https://reforms.ai',
|
344
|
-
name='Unreal Dashboard'
|
345
|
-
)
|
346
|
-
self.source2 = RegistrationSource.objects.create(
|
347
|
-
url='https://app.example.com',
|
348
|
-
name='Example App'
|
349
|
-
)
|
350
|
-
|
351
|
-
def test_user_get_sources(self):
|
352
|
-
"""Test user get_sources method."""
|
353
|
-
# Create user-source relationships
|
354
|
-
UserRegistrationSource.objects.create(
|
355
|
-
user=self.user,
|
356
|
-
source=self.source1,
|
357
|
-
first_registration=True
|
358
|
-
)
|
359
|
-
UserRegistrationSource.objects.create(
|
360
|
-
user=self.user,
|
361
|
-
source=self.source2,
|
362
|
-
first_registration=False
|
363
|
-
)
|
364
|
-
|
365
|
-
sources = self.user.get_sources()
|
366
|
-
self.assertEqual(sources.count(), 2)
|
367
|
-
self.assertIn(self.source1, sources)
|
368
|
-
self.assertIn(self.source2, sources)
|
369
|
-
|
370
|
-
def test_user_get_primary_source(self):
|
371
|
-
"""Test user get_primary_source method."""
|
372
|
-
# Create user-source relationships
|
373
|
-
UserRegistrationSource.objects.create(
|
374
|
-
user=self.user,
|
375
|
-
source=self.source1,
|
376
|
-
first_registration=True
|
377
|
-
)
|
378
|
-
UserRegistrationSource.objects.create(
|
379
|
-
user=self.user,
|
380
|
-
source=self.source2,
|
381
|
-
first_registration=False
|
382
|
-
)
|
383
|
-
|
384
|
-
primary_source = self.user.get_primary_source()
|
385
|
-
self.assertEqual(primary_source, self.source1)
|
386
|
-
|
387
|
-
def test_user_no_sources(self):
|
388
|
-
"""Test user methods when no sources exist."""
|
389
|
-
sources = self.user.get_sources()
|
390
|
-
self.assertEqual(sources.count(), 0)
|
391
|
-
|
392
|
-
primary_source = self.user.get_primary_source()
|
393
|
-
self.assertIsNone(primary_source)
|
394
|
-
|
395
|
-
def test_user_multiple_first_registrations(self):
|
396
|
-
"""Test behavior when multiple sources have first_registration=True."""
|
397
|
-
# Create multiple sources with first_registration=True
|
398
|
-
UserRegistrationSource.objects.create(
|
399
|
-
user=self.user,
|
400
|
-
source=self.source1,
|
401
|
-
first_registration=True
|
402
|
-
)
|
403
|
-
UserRegistrationSource.objects.create(
|
404
|
-
user=self.user,
|
405
|
-
source=self.source2,
|
406
|
-
first_registration=True
|
407
|
-
)
|
408
|
-
|
409
|
-
# Should return the first one (by registration_date)
|
410
|
-
primary_source = self.user.get_primary_source()
|
411
|
-
self.assertIsNotNone(primary_source)
|
412
|
-
self.assertIn(primary_source, [self.source1, self.source2])
|