django-cfg 1.1.50__tar.gz → 1.1.51__tar.gz
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-1.1.50 → django_cfg-1.1.51}/.gitignore +2 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/PKG-INFO +1 -1
- {django_cfg-1.1.50 → django_cfg-1.1.51}/pyproject.toml +1 -1
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/__init__.py +1 -1
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/admin/__init__.py +2 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/admin/filters.py +54 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/admin/otp.py +12 -1
- django_cfg-1.1.51/src/django_cfg/apps/accounts/admin/twilio_response.py +222 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/managers/user_manager.py +16 -0
- django_cfg-1.1.51/src/django_cfg/apps/accounts/migrations/0003_twilioresponse.py +43 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/models.py +93 -0
- django_cfg-1.1.51/src/django_cfg/apps/accounts/serializers/webhook.py +94 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/services/otp_service.py +66 -23
- django_cfg-1.1.51/src/django_cfg/apps/accounts/templates/emails/otp_email.html +213 -0
- django_cfg-1.1.51/src/django_cfg/apps/accounts/templates/emails/otp_email.txt +29 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/urls.py +2 -1
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/utils/notifications.py +88 -17
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/views/__init__.py +3 -0
- django_cfg-1.1.51/src/django_cfg/apps/accounts/views/webhook.py +265 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/api/commands/urls.py +2 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/api/health/urls.py +2 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/tasks/urls.py +1 -1
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/archive/django_sample.zip +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/core/config.py +6 -0
- django_cfg-1.1.51/src/django_cfg/management/commands/list_urls.py +302 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/rundramatiq.py +2 -1
- django_cfg-1.1.51/src/django_cfg/management/commands/show_urls.py +302 -0
- django_cfg-1.1.51/src/django_cfg/management/commands/test_twilio.py +614 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_twilio/__init__.py +46 -38
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_twilio/models.py +34 -16
- django_cfg-1.1.51/src/django_cfg/modules/django_twilio/sendgrid_service.py +504 -0
- django_cfg-1.1.51/src/django_cfg/modules/django_twilio/templates/guide.md +266 -0
- django_cfg-1.1.51/src/django_cfg/modules/django_twilio/templates/sendgrid_otp_email.html +213 -0
- django_cfg-1.1.51/src/django_cfg/modules/django_twilio/templates/sendgrid_test_data.json +14 -0
- django_cfg-1.1.51/src/django_cfg/modules/django_twilio/twilio_service.py +472 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/unfold/dashboard.py +45 -1
- django_cfg-1.1.50/src/django_cfg/apps/accounts/templates/emails/otp_email.html +0 -94
- django_cfg-1.1.50/src/django_cfg/apps/accounts/templates/emails/otp_email.txt +0 -16
- django_cfg-1.1.50/src/django_cfg/examples/README_NGROK.md +0 -186
- django_cfg-1.1.50/src/django_cfg/examples/README_NGROK_ENV.md +0 -194
- django_cfg-1.1.50/src/django_cfg/examples/ngrok_env_example.py +0 -155
- django_cfg-1.1.50/src/django_cfg/examples/ngrok_example.py +0 -75
- django_cfg-1.1.50/src/django_cfg/management/commands/show_urls.py +0 -341
- django_cfg-1.1.50/src/django_cfg/management/commands/test_twilio.py +0 -101
- django_cfg-1.1.50/src/django_cfg/modules/django_twilio/service.py +0 -942
- django_cfg-1.1.50/src/django_cfg/modules/django_twilio/simple_service.py +0 -290
- {django_cfg-1.1.50 → django_cfg-1.1.51}/LICENSE +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/MANIFEST.in +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/README.md +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/requirements-dev.txt +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/requirements-test.txt +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/requirements.txt +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/README.md +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/admin/activity.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/admin/group.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/admin/inlines.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/admin/registration_source.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/admin/user.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/apps.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/management/commands/test_otp.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/managers/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/migrations/0001_initial.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/migrations/0002_add_phone_otp_clean.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/migrations/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/serializers/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/serializers/otp.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/serializers/profile.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/services/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/services/activity_service.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/signals.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/templates/emails/base_email.html +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/templates/emails/base_email.txt +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/templates/emails/welcome_email.html +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/templates/emails/welcome_email.txt +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/views/otp.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/views/profile.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/api/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/api/commands/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/api/commands/views.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/api/health/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/api/health/views.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/leads/README.md +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/leads/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/leads/admin.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/leads/apps.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/leads/migrations/0001_initial.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/leads/migrations/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/leads/models.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/leads/serializers.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/leads/signals.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/leads/tests.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/leads/urls.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/leads/views.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/README.md +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/admin.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/admin_filters.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/apps.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/management/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/management/commands/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/management/commands/test_newsletter.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/managers/README.md +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/managers/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/migrations/0001_initial.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/migrations/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/models.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/serializers.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/services/email_service.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/signals.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/urls.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/utils/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/views/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/views/campaigns.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/views/emails.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/views/newsletters.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/views/subscriptions.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/views/tracking.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/admin.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/admin_filters.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/apps.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/managers/message_manager.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/managers/ticket_manager.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/migrations/0001_initial.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/migrations/0002_alter_message_ticket.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/migrations/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/models.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/serializers.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/signals.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/templates/support/chat/access_denied.html +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/templates/support/chat/ticket_chat.html +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/urls.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/utils/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/utils/support_email_service.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/views/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/views/admin.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/views/api.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/views/chat.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/tasks/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/tasks/admin.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/tasks/apps.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/tasks/templates/tasks/dashboard.html +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/tasks/views.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/urls.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/cli/README.md +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/cli/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/cli/commands/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/cli/commands/create_project.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/cli/commands/info.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/cli/main.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/cli/utils.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/core/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/core/environment.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/core/generation.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/core/validation.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/exceptions.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/integration.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/check_settings.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/create_token.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/generate.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/migrator.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/runserver_ngrok.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/script.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/show_config.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/superuser.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/task_clear.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/task_status.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/test_email.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/test_telegram.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/tree.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/validate_config.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/middleware/README.md +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/middleware/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/middleware/user_activity.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/models/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/models/cache.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/models/constance.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/models/database.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/models/drf.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/models/jwt.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/models/limits.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/models/ngrok.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/models/revolution.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/models/services.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/models/tasks.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/models/unfold.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/base.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_currency/README.md +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_currency/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_currency/cache.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_currency/converter.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_currency/service.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_email.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_llm/README.md +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_llm/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_llm/example.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_llm/llm/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_llm/llm/cache.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_llm/llm/client.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_llm/llm/models_cache.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_llm/service.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_llm/translator/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_llm/translator/cache.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_llm/translator/translator.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_logger.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_ngrok.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_tasks.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_telegram.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_twilio/README.md +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_twilio/exceptions.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/logger.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/unfold/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/unfold/callbacks.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/unfold/models.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/unfold/system_monitor.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/unfold/tailwind.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/routers.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/index.html +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/layouts/dashboard_with_tabs.html +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/components/activity_tracker.html +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/components/charts_section.html +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/components/django_commands.html +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/components/quick_actions.html +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/components/recent_activity.html +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/components/recent_users_table.html +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/components/stats_cards.html +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/components/stats_tiles.html +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/components/system_health.html +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/components/system_metrics.html +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/components/user_permissions.html +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/tabs/app_stats_tab.html +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/tabs/commands_tab.html +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/tabs/overview_tab.html +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/tabs/stats_tab.html +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/tabs/users_tab.html +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/zones/zones_table.html +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/emails/base_email.html +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/utils/__init__.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/utils/path_resolution.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/utils/smart_defaults.py +0 -0
- {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/version_check.py +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.51
|
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
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "django-cfg"
|
7
|
-
version = "1.1.
|
7
|
+
version = "1.1.51"
|
8
8
|
description = "🚀 Production-ready Django configuration framework with type-safe settings, smart automation, and modern developer experience"
|
9
9
|
readme = "README.md"
|
10
10
|
license = {text = "MIT"}
|
@@ -7,6 +7,7 @@ from .otp import OTPSecretAdmin
|
|
7
7
|
from .registration_source import RegistrationSourceAdmin, UserRegistrationSourceAdmin
|
8
8
|
from .activity import UserActivityAdmin
|
9
9
|
from .group import GroupAdmin
|
10
|
+
from .twilio_response import TwilioResponseAdmin
|
10
11
|
|
11
12
|
__all__ = [
|
12
13
|
'CustomUserAdmin',
|
@@ -15,4 +16,5 @@ __all__ = [
|
|
15
16
|
'UserRegistrationSourceAdmin',
|
16
17
|
'UserActivityAdmin',
|
17
18
|
'GroupAdmin',
|
19
|
+
'TwilioResponseAdmin',
|
18
20
|
]
|
@@ -96,3 +96,57 @@ class ActivityTypeFilter(admin.SimpleListFilter):
|
|
96
96
|
elif self.value():
|
97
97
|
return queryset.filter(activity_type=self.value())
|
98
98
|
return queryset
|
99
|
+
|
100
|
+
|
101
|
+
class TwilioResponseStatusFilter(admin.SimpleListFilter):
|
102
|
+
title = "Response Status"
|
103
|
+
parameter_name = "twilio_status"
|
104
|
+
|
105
|
+
def lookups(self, request, model_admin):
|
106
|
+
return (
|
107
|
+
("successful", "Successful"),
|
108
|
+
("failed", "Failed"),
|
109
|
+
("pending", "Pending"),
|
110
|
+
("with_errors", "With Errors"),
|
111
|
+
("recent", "Recent (24h)"),
|
112
|
+
)
|
113
|
+
|
114
|
+
def queryset(self, request, queryset):
|
115
|
+
now = timezone.now()
|
116
|
+
if self.value() == "successful":
|
117
|
+
return queryset.filter(
|
118
|
+
status__in=['sent', 'delivered', 'approved'],
|
119
|
+
error_code__isnull=True
|
120
|
+
)
|
121
|
+
elif self.value() == "failed":
|
122
|
+
return queryset.filter(
|
123
|
+
status__in=['failed', 'undelivered', 'rejected']
|
124
|
+
)
|
125
|
+
elif self.value() == "pending":
|
126
|
+
return queryset.filter(status='pending')
|
127
|
+
elif self.value() == "with_errors":
|
128
|
+
return queryset.exclude(error_code__isnull=True)
|
129
|
+
elif self.value() == "recent":
|
130
|
+
return queryset.filter(created_at__gte=now - timedelta(hours=24))
|
131
|
+
return queryset
|
132
|
+
|
133
|
+
|
134
|
+
class TwilioResponseTypeFilter(admin.SimpleListFilter):
|
135
|
+
title = "Response Type"
|
136
|
+
parameter_name = "twilio_response_type"
|
137
|
+
|
138
|
+
def lookups(self, request, model_admin):
|
139
|
+
return (
|
140
|
+
("api_send", "API Send"),
|
141
|
+
("api_verify", "API Verify"),
|
142
|
+
("webhook_status", "Webhook Status"),
|
143
|
+
("webhook_delivery", "Webhook Delivery"),
|
144
|
+
("otp_related", "OTP Related"),
|
145
|
+
)
|
146
|
+
|
147
|
+
def queryset(self, request, queryset):
|
148
|
+
if self.value() == "otp_related":
|
149
|
+
return queryset.filter(otp_secret__isnull=False)
|
150
|
+
elif self.value():
|
151
|
+
return queryset.filter(response_type=self.value())
|
152
|
+
return queryset
|
@@ -8,16 +8,18 @@ from unfold.admin import ModelAdmin
|
|
8
8
|
|
9
9
|
from ..models import OTPSecret
|
10
10
|
from .filters import OTPStatusFilter
|
11
|
+
from .twilio_response import TwilioResponseInline
|
11
12
|
|
12
13
|
|
13
14
|
@admin.register(OTPSecret)
|
14
15
|
class OTPSecretAdmin(ModelAdmin):
|
15
|
-
list_display = ["recipient", "channel_type", "secret", "status", "created", "expires"]
|
16
|
+
list_display = ["recipient", "channel_type", "secret", "status", "twilio_responses_count", "created", "expires"]
|
16
17
|
list_display_links = ["recipient", "secret"]
|
17
18
|
list_filter = [OTPStatusFilter, "channel_type", "is_used", "created_at"]
|
18
19
|
search_fields = ["recipient", "secret"]
|
19
20
|
readonly_fields = ["created_at", "expires_at"]
|
20
21
|
ordering = ["-created_at"]
|
22
|
+
inlines = [TwilioResponseInline]
|
21
23
|
|
22
24
|
fieldsets = (
|
23
25
|
(
|
@@ -57,3 +59,12 @@ class OTPSecretAdmin(ModelAdmin):
|
|
57
59
|
return naturaltime(obj.expires_at)
|
58
60
|
|
59
61
|
expires.short_description = "Expires"
|
62
|
+
|
63
|
+
def twilio_responses_count(self, obj):
|
64
|
+
"""Count of related Twilio responses."""
|
65
|
+
count = obj.twilio_responses.count()
|
66
|
+
if count == 0:
|
67
|
+
return "—"
|
68
|
+
return f"{count} response{'s' if count != 1 else ''}"
|
69
|
+
|
70
|
+
twilio_responses_count.short_description = "Twilio"
|
@@ -0,0 +1,222 @@
|
|
1
|
+
"""
|
2
|
+
Twilio Response admin configuration.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from django.contrib import admin
|
6
|
+
from django.contrib.humanize.templatetags.humanize import naturaltime
|
7
|
+
from django.utils.html import format_html
|
8
|
+
from unfold.admin import ModelAdmin
|
9
|
+
|
10
|
+
from ..models import TwilioResponse
|
11
|
+
from .filters import TwilioResponseStatusFilter, TwilioResponseTypeFilter
|
12
|
+
|
13
|
+
|
14
|
+
class TwilioResponseInline(admin.TabularInline):
|
15
|
+
"""Inline for showing Twilio responses in related models."""
|
16
|
+
model = TwilioResponse
|
17
|
+
extra = 0
|
18
|
+
readonly_fields = ['created_at', 'status', 'message_sid', 'error_code']
|
19
|
+
fields = ['response_type', 'service_type', 'status', 'message_sid', 'error_code', 'created_at']
|
20
|
+
|
21
|
+
def has_add_permission(self, request, obj=None):
|
22
|
+
return False
|
23
|
+
|
24
|
+
|
25
|
+
@admin.register(TwilioResponse)
|
26
|
+
class TwilioResponseAdmin(ModelAdmin):
|
27
|
+
list_display = [
|
28
|
+
'identifier',
|
29
|
+
'service_type',
|
30
|
+
'response_type',
|
31
|
+
'status_display',
|
32
|
+
'recipient',
|
33
|
+
'price_display',
|
34
|
+
'created_display',
|
35
|
+
'has_error_display'
|
36
|
+
]
|
37
|
+
list_display_links = ['identifier']
|
38
|
+
list_filter = [
|
39
|
+
TwilioResponseStatusFilter,
|
40
|
+
TwilioResponseTypeFilter,
|
41
|
+
'service_type',
|
42
|
+
'response_type',
|
43
|
+
'created_at',
|
44
|
+
]
|
45
|
+
search_fields = [
|
46
|
+
'message_sid',
|
47
|
+
'verification_sid',
|
48
|
+
'to_number',
|
49
|
+
'error_message',
|
50
|
+
'otp_secret__recipient'
|
51
|
+
]
|
52
|
+
readonly_fields = [
|
53
|
+
'created_at',
|
54
|
+
'updated_at',
|
55
|
+
'twilio_created_at',
|
56
|
+
'response_data_display',
|
57
|
+
'request_data_display'
|
58
|
+
]
|
59
|
+
ordering = ['-created_at']
|
60
|
+
|
61
|
+
fieldsets = (
|
62
|
+
(
|
63
|
+
'Basic Information',
|
64
|
+
{
|
65
|
+
'fields': (
|
66
|
+
'response_type',
|
67
|
+
'service_type',
|
68
|
+
'status',
|
69
|
+
'otp_secret'
|
70
|
+
),
|
71
|
+
},
|
72
|
+
),
|
73
|
+
(
|
74
|
+
'Twilio Identifiers',
|
75
|
+
{
|
76
|
+
'fields': (
|
77
|
+
'message_sid',
|
78
|
+
'verification_sid',
|
79
|
+
),
|
80
|
+
},
|
81
|
+
),
|
82
|
+
(
|
83
|
+
'Recipients',
|
84
|
+
{
|
85
|
+
'fields': (
|
86
|
+
'to_number',
|
87
|
+
'from_number',
|
88
|
+
),
|
89
|
+
},
|
90
|
+
),
|
91
|
+
(
|
92
|
+
'Error Information',
|
93
|
+
{
|
94
|
+
'fields': (
|
95
|
+
'error_code',
|
96
|
+
'error_message',
|
97
|
+
),
|
98
|
+
'classes': ('collapse',),
|
99
|
+
},
|
100
|
+
),
|
101
|
+
(
|
102
|
+
'Pricing',
|
103
|
+
{
|
104
|
+
'fields': (
|
105
|
+
'price',
|
106
|
+
'price_unit',
|
107
|
+
),
|
108
|
+
'classes': ('collapse',),
|
109
|
+
},
|
110
|
+
),
|
111
|
+
(
|
112
|
+
'Request/Response Data',
|
113
|
+
{
|
114
|
+
'fields': (
|
115
|
+
'request_data_display',
|
116
|
+
'response_data_display',
|
117
|
+
),
|
118
|
+
'classes': ('collapse',),
|
119
|
+
},
|
120
|
+
),
|
121
|
+
(
|
122
|
+
'Timestamps',
|
123
|
+
{
|
124
|
+
'fields': (
|
125
|
+
'created_at',
|
126
|
+
'updated_at',
|
127
|
+
'twilio_created_at',
|
128
|
+
),
|
129
|
+
'classes': ('collapse',),
|
130
|
+
},
|
131
|
+
),
|
132
|
+
)
|
133
|
+
|
134
|
+
def identifier(self, obj):
|
135
|
+
"""Get the main identifier for the response."""
|
136
|
+
return obj.message_sid or obj.verification_sid or '—'
|
137
|
+
identifier.short_description = 'Identifier'
|
138
|
+
|
139
|
+
def status_display(self, obj):
|
140
|
+
"""Display status with color coding."""
|
141
|
+
if obj.has_error:
|
142
|
+
return format_html(
|
143
|
+
'<span style="color: #dc3545;">❌ {}</span>',
|
144
|
+
obj.status or 'Error'
|
145
|
+
)
|
146
|
+
elif obj.is_successful:
|
147
|
+
return format_html(
|
148
|
+
'<span style="color: #28a745;">✅ {}</span>',
|
149
|
+
obj.status or 'Success'
|
150
|
+
)
|
151
|
+
else:
|
152
|
+
return format_html(
|
153
|
+
'<span style="color: #ffc107;">⏳ {}</span>',
|
154
|
+
obj.status or 'Unknown'
|
155
|
+
)
|
156
|
+
status_display.short_description = 'Status'
|
157
|
+
|
158
|
+
def recipient(self, obj):
|
159
|
+
"""Display recipient with masking for privacy."""
|
160
|
+
if not obj.to_number:
|
161
|
+
return '—'
|
162
|
+
|
163
|
+
# Mask phone numbers and emails for privacy
|
164
|
+
recipient = obj.to_number
|
165
|
+
if '@' in recipient:
|
166
|
+
# Email masking
|
167
|
+
local, domain = recipient.split('@', 1)
|
168
|
+
masked_local = local[:2] + '*' * (len(local) - 2)
|
169
|
+
return f"{masked_local}@{domain}"
|
170
|
+
else:
|
171
|
+
# Phone masking
|
172
|
+
return f"***{recipient[-4:]}" if len(recipient) > 4 else "***"
|
173
|
+
recipient.short_description = 'Recipient'
|
174
|
+
|
175
|
+
def price_display(self, obj):
|
176
|
+
"""Display price with currency."""
|
177
|
+
if obj.price and obj.price_unit:
|
178
|
+
return f"{obj.price} {obj.price_unit.upper()}"
|
179
|
+
return '—'
|
180
|
+
price_display.short_description = 'Price'
|
181
|
+
|
182
|
+
def created_display(self, obj):
|
183
|
+
"""Display created time with natural time."""
|
184
|
+
return naturaltime(obj.created_at)
|
185
|
+
created_display.short_description = 'Created'
|
186
|
+
|
187
|
+
def has_error_display(self, obj):
|
188
|
+
"""Display error status."""
|
189
|
+
if obj.has_error:
|
190
|
+
return format_html('<span style="color: #dc3545;">❌</span>')
|
191
|
+
return format_html('<span style="color: #28a745;">✅</span>')
|
192
|
+
has_error_display.short_description = 'Error'
|
193
|
+
|
194
|
+
def request_data_display(self, obj):
|
195
|
+
"""Display formatted request data."""
|
196
|
+
if not obj.request_data:
|
197
|
+
return '—'
|
198
|
+
|
199
|
+
import json
|
200
|
+
try:
|
201
|
+
formatted = json.dumps(obj.request_data, indent=2, ensure_ascii=False)
|
202
|
+
return format_html('<pre style="font-size: 12px;">{}</pre>', formatted)
|
203
|
+
except (TypeError, ValueError):
|
204
|
+
return str(obj.request_data)
|
205
|
+
request_data_display.short_description = 'Request Data'
|
206
|
+
|
207
|
+
def response_data_display(self, obj):
|
208
|
+
"""Display formatted response data."""
|
209
|
+
if not obj.response_data:
|
210
|
+
return '—'
|
211
|
+
|
212
|
+
import json
|
213
|
+
try:
|
214
|
+
formatted = json.dumps(obj.response_data, indent=2, ensure_ascii=False)
|
215
|
+
return format_html('<pre style="font-size: 12px;">{}</pre>', formatted)
|
216
|
+
except (TypeError, ValueError):
|
217
|
+
return str(obj.response_data)
|
218
|
+
response_data_display.short_description = 'Response Data'
|
219
|
+
|
220
|
+
def get_queryset(self, request):
|
221
|
+
"""Optimize queryset with select_related."""
|
222
|
+
return super().get_queryset(request).select_related('otp_secret')
|
{django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/managers/user_manager.py
RENAMED
@@ -246,6 +246,11 @@ class UserManager(UserManager):
|
|
246
246
|
return user.first_name
|
247
247
|
elif user.last_name:
|
248
248
|
return user.last_name
|
249
|
+
|
250
|
+
# For phone users with temp email, use display_username instead
|
251
|
+
if user.email and user.email.startswith('phone_') and '@temp.' in user.email:
|
252
|
+
return self.get_display_username(user)
|
253
|
+
|
249
254
|
return user.email
|
250
255
|
|
251
256
|
def get_initials(self, user) -> str:
|
@@ -262,6 +267,17 @@ class UserManager(UserManager):
|
|
262
267
|
return user.first_name[0].upper()
|
263
268
|
elif user.last_name:
|
264
269
|
return user.last_name[0].upper()
|
270
|
+
|
271
|
+
# For phone users with temp email, use username initials instead
|
272
|
+
if user.email and user.email.startswith('phone_') and '@temp.' in user.email:
|
273
|
+
if user.username:
|
274
|
+
# Take first two characters of username
|
275
|
+
clean_username = user.username.replace("_", "").replace("-", "").replace(".", "")
|
276
|
+
if len(clean_username) >= 2:
|
277
|
+
return f"{clean_username[0]}{clean_username[1]}".upper()
|
278
|
+
elif len(clean_username) == 1:
|
279
|
+
return clean_username[0].upper()
|
280
|
+
|
265
281
|
return user.email[0].upper()
|
266
282
|
|
267
283
|
def get_display_username(self, user) -> str:
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# Generated by Django 5.2.6 on 2025-09-16 09:23
|
2
|
+
|
3
|
+
import django.db.models.deletion
|
4
|
+
from django.db import migrations, models
|
5
|
+
|
6
|
+
|
7
|
+
class Migration(migrations.Migration):
|
8
|
+
|
9
|
+
dependencies = [
|
10
|
+
('django_cfg_accounts', '0002_add_phone_otp_clean'),
|
11
|
+
]
|
12
|
+
|
13
|
+
operations = [
|
14
|
+
migrations.CreateModel(
|
15
|
+
name='TwilioResponse',
|
16
|
+
fields=[
|
17
|
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
18
|
+
('response_type', models.CharField(choices=[('api_send', 'API Send Request'), ('api_verify', 'API Verify Request'), ('webhook_status', 'Webhook Status Update'), ('webhook_delivery', 'Webhook Delivery Report')], max_length=20)),
|
19
|
+
('service_type', models.CharField(choices=[('whatsapp', 'WhatsApp'), ('sms', 'SMS'), ('voice', 'Voice'), ('email', 'Email'), ('verify', 'Verify API')], max_length=10)),
|
20
|
+
('message_sid', models.CharField(blank=True, help_text='Twilio Message SID', max_length=34)),
|
21
|
+
('verification_sid', models.CharField(blank=True, help_text='Twilio Verification SID', max_length=34)),
|
22
|
+
('request_data', models.JSONField(default=dict, help_text='Original request parameters')),
|
23
|
+
('response_data', models.JSONField(default=dict, help_text='Twilio API response')),
|
24
|
+
('status', models.CharField(blank=True, help_text='Message/Verification status', max_length=20)),
|
25
|
+
('error_code', models.CharField(blank=True, help_text='Twilio error code', max_length=10)),
|
26
|
+
('error_message', models.TextField(blank=True, help_text='Error description')),
|
27
|
+
('to_number', models.CharField(blank=True, help_text='Recipient phone/email', max_length=20)),
|
28
|
+
('from_number', models.CharField(blank=True, help_text='Sender phone/email', max_length=20)),
|
29
|
+
('price', models.DecimalField(blank=True, decimal_places=6, max_digits=10, null=True)),
|
30
|
+
('price_unit', models.CharField(blank=True, help_text='Currency code', max_length=3)),
|
31
|
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
32
|
+
('updated_at', models.DateTimeField(auto_now=True)),
|
33
|
+
('twilio_created_at', models.DateTimeField(blank=True, help_text='Timestamp from Twilio', null=True)),
|
34
|
+
('otp_secret', models.ForeignKey(blank=True, help_text='Related OTP if applicable', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='twilio_responses', to='django_cfg_accounts.otpsecret')),
|
35
|
+
],
|
36
|
+
options={
|
37
|
+
'verbose_name': 'Twilio Response',
|
38
|
+
'verbose_name_plural': 'Twilio Responses',
|
39
|
+
'ordering': ['-created_at'],
|
40
|
+
'indexes': [models.Index(fields=['message_sid'], name='django_cfg__message_c37dcd_idx'), models.Index(fields=['verification_sid'], name='django_cfg__verific_7de689_idx'), models.Index(fields=['status', 'created_at'], name='django_cfg__status_95d8c8_idx'), models.Index(fields=['response_type', 'service_type'], name='django_cfg__respons_20ca26_idx')],
|
41
|
+
},
|
42
|
+
),
|
43
|
+
]
|
@@ -203,6 +203,99 @@ class OTPSecret(models.Model):
|
|
203
203
|
]
|
204
204
|
|
205
205
|
|
206
|
+
class TwilioResponse(models.Model):
|
207
|
+
"""
|
208
|
+
Store Twilio API responses and webhook events.
|
209
|
+
|
210
|
+
This model tracks all interactions with Twilio including:
|
211
|
+
- API responses from sending messages/OTP
|
212
|
+
- Webhook events for delivery status updates
|
213
|
+
- Error tracking and debugging information
|
214
|
+
"""
|
215
|
+
|
216
|
+
RESPONSE_TYPES = [
|
217
|
+
('api_send', 'API Send Request'),
|
218
|
+
('api_verify', 'API Verify Request'),
|
219
|
+
('webhook_status', 'Webhook Status Update'),
|
220
|
+
('webhook_delivery', 'Webhook Delivery Report'),
|
221
|
+
]
|
222
|
+
|
223
|
+
SERVICE_TYPES = [
|
224
|
+
('whatsapp', 'WhatsApp'),
|
225
|
+
('sms', 'SMS'),
|
226
|
+
('voice', 'Voice'),
|
227
|
+
('email', 'Email'),
|
228
|
+
('verify', 'Verify API'),
|
229
|
+
]
|
230
|
+
|
231
|
+
# Basic info
|
232
|
+
response_type = models.CharField(max_length=20, choices=RESPONSE_TYPES)
|
233
|
+
service_type = models.CharField(max_length=10, choices=SERVICE_TYPES)
|
234
|
+
|
235
|
+
# Twilio identifiers
|
236
|
+
message_sid = models.CharField(max_length=34, blank=True, help_text="Twilio Message SID")
|
237
|
+
verification_sid = models.CharField(max_length=34, blank=True, help_text="Twilio Verification SID")
|
238
|
+
|
239
|
+
# Request/Response data
|
240
|
+
request_data = models.JSONField(default=dict, help_text="Original request parameters")
|
241
|
+
response_data = models.JSONField(default=dict, help_text="Twilio API response")
|
242
|
+
|
243
|
+
# Status tracking
|
244
|
+
status = models.CharField(max_length=20, blank=True, help_text="Message/Verification status")
|
245
|
+
error_code = models.CharField(max_length=10, blank=True, help_text="Twilio error code")
|
246
|
+
error_message = models.TextField(blank=True, help_text="Error description")
|
247
|
+
|
248
|
+
# Recipient info
|
249
|
+
to_number = models.CharField(max_length=20, blank=True, help_text="Recipient phone/email")
|
250
|
+
from_number = models.CharField(max_length=20, blank=True, help_text="Sender phone/email")
|
251
|
+
|
252
|
+
# Pricing
|
253
|
+
price = models.DecimalField(max_digits=10, decimal_places=6, null=True, blank=True)
|
254
|
+
price_unit = models.CharField(max_length=3, blank=True, help_text="Currency code")
|
255
|
+
|
256
|
+
# Timestamps
|
257
|
+
created_at = models.DateTimeField(auto_now_add=True)
|
258
|
+
updated_at = models.DateTimeField(auto_now=True)
|
259
|
+
twilio_created_at = models.DateTimeField(null=True, blank=True, help_text="Timestamp from Twilio")
|
260
|
+
|
261
|
+
# Relations
|
262
|
+
otp_secret = models.ForeignKey(
|
263
|
+
'OTPSecret',
|
264
|
+
on_delete=models.SET_NULL,
|
265
|
+
null=True,
|
266
|
+
blank=True,
|
267
|
+
related_name='twilio_responses',
|
268
|
+
help_text="Related OTP if applicable"
|
269
|
+
)
|
270
|
+
|
271
|
+
class Meta:
|
272
|
+
app_label = 'django_cfg_accounts'
|
273
|
+
verbose_name = 'Twilio Response'
|
274
|
+
verbose_name_plural = 'Twilio Responses'
|
275
|
+
ordering = ['-created_at']
|
276
|
+
indexes = [
|
277
|
+
models.Index(fields=['message_sid']),
|
278
|
+
models.Index(fields=['verification_sid']),
|
279
|
+
models.Index(fields=['status', 'created_at']),
|
280
|
+
models.Index(fields=['response_type', 'service_type']),
|
281
|
+
]
|
282
|
+
|
283
|
+
def __str__(self):
|
284
|
+
identifier = self.message_sid or self.verification_sid or 'Unknown'
|
285
|
+
return f"{self.get_service_type_display()} {self.get_response_type_display()} - {identifier}"
|
286
|
+
|
287
|
+
@property
|
288
|
+
def is_successful(self):
|
289
|
+
"""Check if the response indicates success."""
|
290
|
+
success_statuses = ['sent', 'delivered', 'pending', 'approved']
|
291
|
+
return self.status.lower() in success_statuses if self.status else False
|
292
|
+
|
293
|
+
@property
|
294
|
+
def has_error(self):
|
295
|
+
"""Check if the response has an error."""
|
296
|
+
return bool(self.error_code or self.error_message)
|
297
|
+
|
298
|
+
|
206
299
|
class UserActivity(models.Model):
|
207
300
|
"""
|
208
301
|
User activity log.
|
@@ -0,0 +1,94 @@
|
|
1
|
+
"""
|
2
|
+
Twilio Webhook Serializers
|
3
|
+
|
4
|
+
Serializers for validating and processing Twilio webhook data.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from rest_framework import serializers
|
8
|
+
|
9
|
+
|
10
|
+
class TwilioWebhookSerializer(serializers.Serializer):
|
11
|
+
"""
|
12
|
+
Serializer for Twilio webhook data.
|
13
|
+
|
14
|
+
This handles both message status webhooks and verification webhooks
|
15
|
+
from Twilio. The fields are optional because different webhook types
|
16
|
+
send different data.
|
17
|
+
"""
|
18
|
+
|
19
|
+
# Message-related fields (SMS/WhatsApp)
|
20
|
+
MessageSid = serializers.CharField(required=False, help_text="Twilio Message SID")
|
21
|
+
MessageStatus = serializers.CharField(required=False, help_text="Message status (sent, delivered, failed, etc.)")
|
22
|
+
To = serializers.CharField(required=False, help_text="Recipient phone number")
|
23
|
+
From = serializers.CharField(required=False, help_text="Sender phone number")
|
24
|
+
Body = serializers.CharField(required=False, help_text="Message body")
|
25
|
+
|
26
|
+
# Error fields
|
27
|
+
ErrorCode = serializers.CharField(required=False, help_text="Twilio error code")
|
28
|
+
ErrorMessage = serializers.CharField(required=False, help_text="Error message description")
|
29
|
+
|
30
|
+
# Pricing fields
|
31
|
+
Price = serializers.DecimalField(max_digits=10, decimal_places=6, required=False, help_text="Message price")
|
32
|
+
PriceUnit = serializers.CharField(required=False, help_text="Currency code")
|
33
|
+
|
34
|
+
# Verification-related fields (Verify API)
|
35
|
+
VerificationSid = serializers.CharField(required=False, help_text="Twilio Verification SID")
|
36
|
+
VerificationStatus = serializers.CharField(required=False, help_text="Verification status (approved, canceled, etc.)")
|
37
|
+
Channel = serializers.CharField(required=False, help_text="Verification channel (sms, whatsapp, call)")
|
38
|
+
|
39
|
+
# Timestamp fields
|
40
|
+
DateCreated = serializers.DateTimeField(required=False, help_text="When the message was created")
|
41
|
+
DateSent = serializers.DateTimeField(required=False, help_text="When the message was sent")
|
42
|
+
DateUpdated = serializers.DateTimeField(required=False, help_text="When the status was last updated")
|
43
|
+
|
44
|
+
# Account information
|
45
|
+
AccountSid = serializers.CharField(required=False, help_text="Twilio Account SID")
|
46
|
+
|
47
|
+
# Additional fields that might be present
|
48
|
+
Direction = serializers.CharField(required=False, help_text="Message direction (inbound/outbound)")
|
49
|
+
ApiVersion = serializers.CharField(required=False, help_text="Twilio API version")
|
50
|
+
|
51
|
+
# Alternative field names (some webhooks use different casing)
|
52
|
+
message_sid = serializers.CharField(required=False, help_text="Alternative field name for MessageSid")
|
53
|
+
message_status = serializers.CharField(required=False, help_text="Alternative field name for MessageStatus")
|
54
|
+
verification_sid = serializers.CharField(required=False, help_text="Alternative field name for VerificationSid")
|
55
|
+
verification_status = serializers.CharField(required=False, help_text="Alternative field name for VerificationStatus")
|
56
|
+
|
57
|
+
def validate(self, data):
|
58
|
+
"""
|
59
|
+
Ensure that we have at least one of the required identifiers.
|
60
|
+
"""
|
61
|
+
message_sid = data.get('MessageSid') or data.get('message_sid')
|
62
|
+
verification_sid = data.get('VerificationSid') or data.get('verification_sid')
|
63
|
+
|
64
|
+
if not message_sid and not verification_sid:
|
65
|
+
raise serializers.ValidationError(
|
66
|
+
"Either MessageSid or VerificationSid must be provided"
|
67
|
+
)
|
68
|
+
|
69
|
+
return data
|
70
|
+
|
71
|
+
def to_internal_value(self, data):
|
72
|
+
"""
|
73
|
+
Convert the webhook data to internal format.
|
74
|
+
|
75
|
+
This handles the fact that Twilio webhooks are sent as form data,
|
76
|
+
not JSON, so we need to be flexible about the input format.
|
77
|
+
"""
|
78
|
+
if hasattr(data, 'items'):
|
79
|
+
# Convert QueryDict or dict to regular dict
|
80
|
+
data = dict(data.items()) if hasattr(data, 'items') else data
|
81
|
+
|
82
|
+
return super().to_internal_value(data)
|
83
|
+
|
84
|
+
|
85
|
+
class TwilioWebhookResponseSerializer(serializers.Serializer):
|
86
|
+
"""Response serializer for webhook endpoints."""
|
87
|
+
status = serializers.CharField(help_text="Processing status")
|
88
|
+
message = serializers.CharField(required=False, help_text="Optional message")
|
89
|
+
|
90
|
+
|
91
|
+
class TwilioWebhookErrorSerializer(serializers.Serializer):
|
92
|
+
"""Error response serializer for webhook endpoints."""
|
93
|
+
error = serializers.CharField(help_text="Error description")
|
94
|
+
details = serializers.DictField(required=False, help_text="Additional error details")
|