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.
Files changed (28) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/management/commands/rundramatiq.py +174 -202
  3. django_cfg/modules/django_tasks.py +54 -428
  4. django_cfg/modules/dramatiq_setup.py +16 -0
  5. {django_cfg-1.1.61.dist-info → django_cfg-1.1.63.dist-info}/METADATA +145 -4
  6. {django_cfg-1.1.61.dist-info → django_cfg-1.1.63.dist-info}/RECORD +9 -27
  7. django_cfg/apps/accounts/tests/__init__.py +0 -1
  8. django_cfg/apps/accounts/tests/test_models.py +0 -412
  9. django_cfg/apps/accounts/tests/test_otp_views.py +0 -143
  10. django_cfg/apps/accounts/tests/test_serializers.py +0 -331
  11. django_cfg/apps/accounts/tests/test_services.py +0 -401
  12. django_cfg/apps/accounts/tests/test_signals.py +0 -110
  13. django_cfg/apps/accounts/tests/test_views.py +0 -255
  14. django_cfg/apps/newsletter/tests/__init__.py +0 -1
  15. django_cfg/apps/newsletter/tests/run_tests.py +0 -47
  16. django_cfg/apps/newsletter/tests/test_email_integration.py +0 -256
  17. django_cfg/apps/newsletter/tests/test_email_tracking.py +0 -332
  18. django_cfg/apps/newsletter/tests/test_newsletter_manager.py +0 -83
  19. django_cfg/apps/newsletter/tests/test_newsletter_models.py +0 -157
  20. django_cfg/apps/support/tests/__init__.py +0 -0
  21. django_cfg/apps/support/tests/test_models.py +0 -106
  22. django_cfg/apps/tasks/@docs/CONFIGURATION.md +0 -663
  23. django_cfg/apps/tasks/@docs/README.md +0 -195
  24. django_cfg/apps/tasks/@docs/TASKS_QUEUES.md +0 -423
  25. django_cfg/apps/tasks/@docs/TROUBLESHOOTING.md +0 -506
  26. {django_cfg-1.1.61.dist-info → django_cfg-1.1.63.dist-info}/WHEEL +0 -0
  27. {django_cfg-1.1.61.dist-info → django_cfg-1.1.63.dist-info}/entry_points.txt +0 -0
  28. {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.61
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
- Built-in Dramatiq integration for reliable background job processing with Redis.
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=LLAKMB4NP27tQ_Kza-viPCeZf8gKcWhpQZIH8AcQOJo,14288
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=EUSLLaE2XZ7HCghwN3E2P6jrtziz8HpETN0uydKvXT8,10049
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=SOqwphvg_jlUjYmZG7o5mfePJkcwLW0m9RKO4ed7lBs,27322
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.61.dist-info/METADATA,sha256=9MfvaMizPU_nhP68eF52gJt0P9alC1aKyjwb8nwwU6o,38953
274
- django_cfg-1.1.61.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
275
- django_cfg-1.1.61.dist-info/entry_points.txt,sha256=Ucmde4Z2wEzgb4AggxxZ0zaYDb9HpyE5blM3uJ0_VNg,56
276
- django_cfg-1.1.61.dist-info/licenses/LICENSE,sha256=xHuytiUkSZCRG3N11nk1X6q1_EGQtv6aL5O9cqNRhKE,1071
277
- django_cfg-1.1.61.dist-info/RECORD,,
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])