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.
Files changed (248) hide show
  1. {django_cfg-1.1.50 → django_cfg-1.1.51}/.gitignore +2 -0
  2. {django_cfg-1.1.50 → django_cfg-1.1.51}/PKG-INFO +1 -1
  3. {django_cfg-1.1.50 → django_cfg-1.1.51}/pyproject.toml +1 -1
  4. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/__init__.py +1 -1
  5. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/admin/__init__.py +2 -0
  6. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/admin/filters.py +54 -0
  7. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/admin/otp.py +12 -1
  8. django_cfg-1.1.51/src/django_cfg/apps/accounts/admin/twilio_response.py +222 -0
  9. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/managers/user_manager.py +16 -0
  10. django_cfg-1.1.51/src/django_cfg/apps/accounts/migrations/0003_twilioresponse.py +43 -0
  11. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/models.py +93 -0
  12. django_cfg-1.1.51/src/django_cfg/apps/accounts/serializers/webhook.py +94 -0
  13. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/services/otp_service.py +66 -23
  14. django_cfg-1.1.51/src/django_cfg/apps/accounts/templates/emails/otp_email.html +213 -0
  15. django_cfg-1.1.51/src/django_cfg/apps/accounts/templates/emails/otp_email.txt +29 -0
  16. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/urls.py +2 -1
  17. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/utils/notifications.py +88 -17
  18. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/views/__init__.py +3 -0
  19. django_cfg-1.1.51/src/django_cfg/apps/accounts/views/webhook.py +265 -0
  20. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/api/commands/urls.py +2 -0
  21. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/api/health/urls.py +2 -0
  22. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/tasks/urls.py +1 -1
  23. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/archive/django_sample.zip +0 -0
  24. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/core/config.py +6 -0
  25. django_cfg-1.1.51/src/django_cfg/management/commands/list_urls.py +302 -0
  26. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/rundramatiq.py +2 -1
  27. django_cfg-1.1.51/src/django_cfg/management/commands/show_urls.py +302 -0
  28. django_cfg-1.1.51/src/django_cfg/management/commands/test_twilio.py +614 -0
  29. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_twilio/__init__.py +46 -38
  30. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_twilio/models.py +34 -16
  31. django_cfg-1.1.51/src/django_cfg/modules/django_twilio/sendgrid_service.py +504 -0
  32. django_cfg-1.1.51/src/django_cfg/modules/django_twilio/templates/guide.md +266 -0
  33. django_cfg-1.1.51/src/django_cfg/modules/django_twilio/templates/sendgrid_otp_email.html +213 -0
  34. django_cfg-1.1.51/src/django_cfg/modules/django_twilio/templates/sendgrid_test_data.json +14 -0
  35. django_cfg-1.1.51/src/django_cfg/modules/django_twilio/twilio_service.py +472 -0
  36. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/unfold/dashboard.py +45 -1
  37. django_cfg-1.1.50/src/django_cfg/apps/accounts/templates/emails/otp_email.html +0 -94
  38. django_cfg-1.1.50/src/django_cfg/apps/accounts/templates/emails/otp_email.txt +0 -16
  39. django_cfg-1.1.50/src/django_cfg/examples/README_NGROK.md +0 -186
  40. django_cfg-1.1.50/src/django_cfg/examples/README_NGROK_ENV.md +0 -194
  41. django_cfg-1.1.50/src/django_cfg/examples/ngrok_env_example.py +0 -155
  42. django_cfg-1.1.50/src/django_cfg/examples/ngrok_example.py +0 -75
  43. django_cfg-1.1.50/src/django_cfg/management/commands/show_urls.py +0 -341
  44. django_cfg-1.1.50/src/django_cfg/management/commands/test_twilio.py +0 -101
  45. django_cfg-1.1.50/src/django_cfg/modules/django_twilio/service.py +0 -942
  46. django_cfg-1.1.50/src/django_cfg/modules/django_twilio/simple_service.py +0 -290
  47. {django_cfg-1.1.50 → django_cfg-1.1.51}/LICENSE +0 -0
  48. {django_cfg-1.1.50 → django_cfg-1.1.51}/MANIFEST.in +0 -0
  49. {django_cfg-1.1.50 → django_cfg-1.1.51}/README.md +0 -0
  50. {django_cfg-1.1.50 → django_cfg-1.1.51}/requirements-dev.txt +0 -0
  51. {django_cfg-1.1.50 → django_cfg-1.1.51}/requirements-test.txt +0 -0
  52. {django_cfg-1.1.50 → django_cfg-1.1.51}/requirements.txt +0 -0
  53. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/__init__.py +0 -0
  54. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/README.md +0 -0
  55. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/__init__.py +0 -0
  56. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/admin/activity.py +0 -0
  57. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/admin/group.py +0 -0
  58. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/admin/inlines.py +0 -0
  59. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/admin/registration_source.py +0 -0
  60. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/admin/user.py +0 -0
  61. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/apps.py +0 -0
  62. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/management/commands/test_otp.py +0 -0
  63. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/managers/__init__.py +0 -0
  64. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/migrations/0001_initial.py +0 -0
  65. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/migrations/0002_add_phone_otp_clean.py +0 -0
  66. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/migrations/__init__.py +0 -0
  67. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/serializers/__init__.py +0 -0
  68. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/serializers/otp.py +0 -0
  69. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/serializers/profile.py +0 -0
  70. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/services/__init__.py +0 -0
  71. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/services/activity_service.py +0 -0
  72. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/signals.py +0 -0
  73. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/templates/emails/base_email.html +0 -0
  74. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/templates/emails/base_email.txt +0 -0
  75. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/templates/emails/welcome_email.html +0 -0
  76. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/templates/emails/welcome_email.txt +0 -0
  77. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/views/otp.py +0 -0
  78. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/accounts/views/profile.py +0 -0
  79. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/api/__init__.py +0 -0
  80. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/api/commands/__init__.py +0 -0
  81. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/api/commands/views.py +0 -0
  82. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/api/health/__init__.py +0 -0
  83. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/api/health/views.py +0 -0
  84. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/leads/README.md +0 -0
  85. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/leads/__init__.py +0 -0
  86. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/leads/admin.py +0 -0
  87. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/leads/apps.py +0 -0
  88. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/leads/migrations/0001_initial.py +0 -0
  89. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/leads/migrations/__init__.py +0 -0
  90. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/leads/models.py +0 -0
  91. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/leads/serializers.py +0 -0
  92. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/leads/signals.py +0 -0
  93. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/leads/tests.py +0 -0
  94. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/leads/urls.py +0 -0
  95. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/leads/views.py +0 -0
  96. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/README.md +0 -0
  97. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/__init__.py +0 -0
  98. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/admin.py +0 -0
  99. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/admin_filters.py +0 -0
  100. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/apps.py +0 -0
  101. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/management/__init__.py +0 -0
  102. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/management/commands/__init__.py +0 -0
  103. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/management/commands/test_newsletter.py +0 -0
  104. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/managers/README.md +0 -0
  105. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/managers/__init__.py +0 -0
  106. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/migrations/0001_initial.py +0 -0
  107. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/migrations/__init__.py +0 -0
  108. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/models.py +0 -0
  109. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/serializers.py +0 -0
  110. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/services/email_service.py +0 -0
  111. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/signals.py +0 -0
  112. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/urls.py +0 -0
  113. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/utils/__init__.py +0 -0
  114. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/views/__init__.py +0 -0
  115. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/views/campaigns.py +0 -0
  116. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/views/emails.py +0 -0
  117. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/views/newsletters.py +0 -0
  118. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/views/subscriptions.py +0 -0
  119. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/newsletter/views/tracking.py +0 -0
  120. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/__init__.py +0 -0
  121. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/admin.py +0 -0
  122. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/admin_filters.py +0 -0
  123. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/apps.py +0 -0
  124. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/managers/message_manager.py +0 -0
  125. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/managers/ticket_manager.py +0 -0
  126. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/migrations/0001_initial.py +0 -0
  127. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/migrations/0002_alter_message_ticket.py +0 -0
  128. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/migrations/__init__.py +0 -0
  129. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/models.py +0 -0
  130. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/serializers.py +0 -0
  131. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/signals.py +0 -0
  132. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/templates/support/chat/access_denied.html +0 -0
  133. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/templates/support/chat/ticket_chat.html +0 -0
  134. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/urls.py +0 -0
  135. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/utils/__init__.py +0 -0
  136. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/utils/support_email_service.py +0 -0
  137. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/views/__init__.py +0 -0
  138. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/views/admin.py +0 -0
  139. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/views/api.py +0 -0
  140. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/support/views/chat.py +0 -0
  141. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/tasks/__init__.py +0 -0
  142. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/tasks/admin.py +0 -0
  143. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/tasks/apps.py +0 -0
  144. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/tasks/templates/tasks/dashboard.html +0 -0
  145. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/tasks/views.py +0 -0
  146. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps/urls.py +0 -0
  147. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/apps.py +0 -0
  148. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/cli/README.md +0 -0
  149. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/cli/__init__.py +0 -0
  150. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/cli/commands/__init__.py +0 -0
  151. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/cli/commands/create_project.py +0 -0
  152. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/cli/commands/info.py +0 -0
  153. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/cli/main.py +0 -0
  154. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/cli/utils.py +0 -0
  155. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/core/__init__.py +0 -0
  156. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/core/environment.py +0 -0
  157. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/core/generation.py +0 -0
  158. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/core/validation.py +0 -0
  159. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/exceptions.py +0 -0
  160. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/integration.py +0 -0
  161. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/__init__.py +0 -0
  162. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/__init__.py +0 -0
  163. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/check_settings.py +0 -0
  164. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/create_token.py +0 -0
  165. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/generate.py +0 -0
  166. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/migrator.py +0 -0
  167. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/runserver_ngrok.py +0 -0
  168. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/script.py +0 -0
  169. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/show_config.py +0 -0
  170. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/superuser.py +0 -0
  171. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/task_clear.py +0 -0
  172. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/task_status.py +0 -0
  173. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/test_email.py +0 -0
  174. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/test_telegram.py +0 -0
  175. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/tree.py +0 -0
  176. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/management/commands/validate_config.py +0 -0
  177. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/middleware/README.md +0 -0
  178. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/middleware/__init__.py +0 -0
  179. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/middleware/user_activity.py +0 -0
  180. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/models/__init__.py +0 -0
  181. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/models/cache.py +0 -0
  182. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/models/constance.py +0 -0
  183. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/models/database.py +0 -0
  184. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/models/drf.py +0 -0
  185. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/models/jwt.py +0 -0
  186. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/models/limits.py +0 -0
  187. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/models/ngrok.py +0 -0
  188. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/models/revolution.py +0 -0
  189. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/models/services.py +0 -0
  190. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/models/tasks.py +0 -0
  191. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/models/unfold.py +0 -0
  192. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/__init__.py +0 -0
  193. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/base.py +0 -0
  194. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_currency/README.md +0 -0
  195. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_currency/__init__.py +0 -0
  196. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_currency/cache.py +0 -0
  197. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_currency/converter.py +0 -0
  198. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_currency/service.py +0 -0
  199. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_email.py +0 -0
  200. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_llm/README.md +0 -0
  201. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_llm/__init__.py +0 -0
  202. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_llm/example.py +0 -0
  203. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_llm/llm/__init__.py +0 -0
  204. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_llm/llm/cache.py +0 -0
  205. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_llm/llm/client.py +0 -0
  206. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_llm/llm/models_cache.py +0 -0
  207. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_llm/service.py +0 -0
  208. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_llm/translator/__init__.py +0 -0
  209. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_llm/translator/cache.py +0 -0
  210. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_llm/translator/translator.py +0 -0
  211. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_logger.py +0 -0
  212. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_ngrok.py +0 -0
  213. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_tasks.py +0 -0
  214. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_telegram.py +0 -0
  215. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_twilio/README.md +0 -0
  216. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/django_twilio/exceptions.py +0 -0
  217. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/logger.py +0 -0
  218. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/unfold/__init__.py +0 -0
  219. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/unfold/callbacks.py +0 -0
  220. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/unfold/models.py +0 -0
  221. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/unfold/system_monitor.py +0 -0
  222. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/modules/unfold/tailwind.py +0 -0
  223. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/routers.py +0 -0
  224. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/__init__.py +0 -0
  225. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/index.html +0 -0
  226. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/layouts/dashboard_with_tabs.html +0 -0
  227. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/components/activity_tracker.html +0 -0
  228. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/components/charts_section.html +0 -0
  229. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/components/django_commands.html +0 -0
  230. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/components/quick_actions.html +0 -0
  231. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/components/recent_activity.html +0 -0
  232. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/components/recent_users_table.html +0 -0
  233. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/components/stats_cards.html +0 -0
  234. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/components/stats_tiles.html +0 -0
  235. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/components/system_health.html +0 -0
  236. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/components/system_metrics.html +0 -0
  237. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/components/user_permissions.html +0 -0
  238. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/tabs/app_stats_tab.html +0 -0
  239. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/tabs/commands_tab.html +0 -0
  240. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/tabs/overview_tab.html +0 -0
  241. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/tabs/stats_tab.html +0 -0
  242. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/tabs/users_tab.html +0 -0
  243. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/admin/snippets/zones/zones_table.html +0 -0
  244. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/templates/emails/base_email.html +0 -0
  245. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/utils/__init__.py +0 -0
  246. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/utils/path_resolution.py +0 -0
  247. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/utils/smart_defaults.py +0 -0
  248. {django_cfg-1.1.50 → django_cfg-1.1.51}/src/django_cfg/version_check.py +0 -0
@@ -76,3 +76,5 @@ package-lock.json
76
76
  *.bak
77
77
  *.tmp
78
78
  *.temp
79
+
80
+ /cache
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-cfg
3
- Version: 1.1.50
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.50"
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"}
@@ -38,7 +38,7 @@ default_app_config = "django_cfg.apps.DjangoCfgConfig"
38
38
  from typing import TYPE_CHECKING
39
39
 
40
40
  # Version information
41
- __version__ = "1.1.50"
41
+ __version__ = "1.1.51"
42
42
  __author__ = "ReformsAI Team"
43
43
  __email__ = "info@reforms.ai"
44
44
  __license__ = "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')
@@ -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")