django-pfx 1.7.6.dev21__tar.gz → 1.7.6.dev25__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 (326) hide show
  1. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/PKG-INFO +1 -1
  2. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/django_pfx.egg-info/PKG-INFO +1 -1
  3. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/django_pfx.egg-info/SOURCES.txt +6 -0
  4. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/django_pfx.egg-info/scm_file_list.json +6 -0
  5. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/django_pfx.egg-info/scm_version.json +2 -2
  6. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/doc/source/authentication.md +2 -0
  7. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/doc/source/testing.md +90 -0
  8. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/emails/email_backend/__init__.py +1 -0
  9. django_pfx-1.7.6.dev25/pfx/emails/email_backend/memory_email_backend.py +14 -0
  10. django_pfx-1.7.6.dev25/pfx/emails/test.py +40 -0
  11. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/fields/media_field.py +31 -3
  12. django_pfx-1.7.6.dev25/pfx/pfxcore/sms/backends/memory.py +15 -0
  13. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/test.py +99 -0
  14. django_pfx-1.7.6.dev25/pfx/pfxcore/xml_test_runner.py +8 -0
  15. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/basic_api_test.py +31 -0
  16. django_pfx-1.7.6.dev25/tests/tests/test_emails_test_tools.py +60 -0
  17. django_pfx-1.7.6.dev25/tests/tests/test_test_runner.py +117 -0
  18. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/.gitignore +0 -0
  19. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/.gitlab-ci.yml +0 -0
  20. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/.pre-commit-config.yaml +0 -0
  21. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/LICENSE +0 -0
  22. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/MANIFEST.in +0 -0
  23. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/README.md +0 -0
  24. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/django_pfx.egg-info/dependency_links.txt +0 -0
  25. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/django_pfx.egg-info/requires.txt +0 -0
  26. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/django_pfx.egg-info/top_level.txt +0 -0
  27. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/doc/Makefile +0 -0
  28. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/doc/conf.py +0 -0
  29. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/doc/index.rst +0 -0
  30. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/doc/source/api.views.rst +0 -0
  31. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/doc/source/decorator.md +0 -0
  32. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/doc/source/generate_openapi.md +0 -0
  33. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/doc/source/getting_started.md +0 -0
  34. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/doc/source/internationalisation.md +0 -0
  35. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/doc/source/model.md +0 -0
  36. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/doc/source/pfx_views.md +0 -0
  37. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/doc/source/profiling.md +0 -0
  38. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/doc/source/settings.md +0 -0
  39. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/doc/source/user_jobs.md +0 -0
  40. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/img/pfx.png +0 -0
  41. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/img/pfx.svg +0 -0
  42. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/make_messages +0 -0
  43. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/manage.py +0 -0
  44. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/__init__.py +0 -0
  45. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/emails/__init__.py +0 -0
  46. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/emails/apps.py +0 -0
  47. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/emails/email_backend/q_mailjet_email_backend.py +0 -0
  48. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/emails/exceptions.py +0 -0
  49. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/emails/locale/fr/LC_MESSAGES/django.mo +0 -0
  50. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/emails/locale/fr/LC_MESSAGES/django.po +0 -0
  51. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/emails/mailjet/__init__.py +0 -0
  52. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/emails/mailjet/mailjet.py +0 -0
  53. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/emails/mailjet/message_result.py +0 -0
  54. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/emails/migrations/0001_initial.py +0 -0
  55. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/emails/migrations/0002_alter_email_headers.py +0 -0
  56. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/emails/migrations/0003_alter_email_options.py +0 -0
  57. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/emails/migrations/0004_alter_email_mail_from.py +0 -0
  58. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/emails/migrations/0005_lowercase_verbose_names.py +0 -0
  59. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/emails/migrations/__init__.py +0 -0
  60. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/emails/models/__init__.py +0 -0
  61. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/emails/models/email.py +0 -0
  62. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/emails/providers/__init__.py +0 -0
  63. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/emails/providers/email_django_q_executor.py +0 -0
  64. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/emails/shortcuts.py +0 -0
  65. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/emails/tasks.py +0 -0
  66. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/emails/templates/emails/base_template.html +0 -0
  67. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/emails/templates/emails/base_template.txt +0 -0
  68. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/emails/templates/emails/base_template_subject.txt +0 -0
  69. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/emails/urls.py +0 -0
  70. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/emails/views/__init__.py +0 -0
  71. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/emails/views/email_rest_view.py +0 -0
  72. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/__init__.py +0 -0
  73. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/apidoc/__init__.py +0 -0
  74. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/apidoc/parameters.py +0 -0
  75. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/apidoc/schema.py +0 -0
  76. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/apidoc/tags.py +0 -0
  77. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/apps.py +0 -0
  78. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/decorator/__init__.py +0 -0
  79. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/decorator/rest.py +0 -0
  80. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/default_settings.py +0 -0
  81. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/exceptions.py +0 -0
  82. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/fields/__init__.py +0 -0
  83. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/fields/decimal_field.py +0 -0
  84. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/fields/minutes_duration_field.py +0 -0
  85. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/fields/rich_text_field.py +0 -0
  86. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/http/__init__.py +0 -0
  87. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/http/json_response.py +0 -0
  88. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
  89. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.po +0 -0
  90. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/management/__init__.py +0 -0
  91. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/management/commands/__init__.py +0 -0
  92. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/management/commands/makeapidoc.py +0 -0
  93. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/management/commands/profile.py +0 -0
  94. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/mfa/__init__.py +0 -0
  95. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/mfa/authenticator.py +0 -0
  96. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/mfa/base.py +0 -0
  97. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/mfa/email.py +0 -0
  98. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/mfa/legacy.py +0 -0
  99. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/mfa/sms.py +0 -0
  100. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/middleware/__init__.py +0 -0
  101. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/middleware/authentication.py +0 -0
  102. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/middleware/locale.py +0 -0
  103. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/middleware/profiling.py +0 -0
  104. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/middleware/request_too_big_middleware.py +0 -0
  105. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/middleware/ws_cookie_authentication_middleware.py +0 -0
  106. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/migrations/0001_initial.py +0 -0
  107. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/migrations/0002_pfxpermissionsuser.py +0 -0
  108. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/migrations/0003_delete_pfxpermissionsuser.py +0 -0
  109. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/migrations/0004_alter_loginban_failed_counter_and_more.py +0 -0
  110. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/migrations/0005_mfadevice_mfadevice_mfa_device_unique_backend.py +0 -0
  111. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/migrations/0006_alter_pfxuser_managers.py +0 -0
  112. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/migrations/0007_alter_pfxuser_options.py +0 -0
  113. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/migrations/0008_lowercase_verbose_names.py +0 -0
  114. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/migrations/__init__.py +0 -0
  115. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/migrations/operations/__init__.py +0 -0
  116. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/migrations/operations/permissions.py +0 -0
  117. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/models/__init__.py +0 -0
  118. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/models/abstract_pfx_base_user.py +0 -0
  119. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/models/attachment_mixin.py +0 -0
  120. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/models/cache_mixins.py +0 -0
  121. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/models/login_ban.py +0 -0
  122. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/models/mfa_device.py +0 -0
  123. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/models/mfa_user_mixin.py +0 -0
  124. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/models/not_null_fields.py +0 -0
  125. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/models/ordered_model_mixin.py +0 -0
  126. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/models/pfx_models.py +0 -0
  127. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/models/pfx_user.py +0 -0
  128. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/models/user_filtered_queryset_mixin.py +0 -0
  129. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/serializers/__init__.py +0 -0
  130. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/serializers/json.py +0 -0
  131. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/settings.py +0 -0
  132. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/shortcuts.py +0 -0
  133. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/sms/__init__.py +0 -0
  134. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/sms/backends/__init__.py +0 -0
  135. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/sms/backends/base.py +0 -0
  136. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/sms/backends/console.py +0 -0
  137. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/storage/__init__.py +0 -0
  138. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/storage/exceptions.py +0 -0
  139. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/storage/local_storage.py +0 -0
  140. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/storage/s3_storage.py +0 -0
  141. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/templates/registration/otp_code_email.txt +0 -0
  142. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/templates/registration/otp_code_subject.txt +0 -0
  143. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/templates/registration/password_reset_email.txt +0 -0
  144. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/templates/registration/password_reset_subject.txt +0 -0
  145. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/templates/registration/welcome_email.txt +0 -0
  146. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/templates/registration/welcome_subject.txt +0 -0
  147. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/urls.py +0 -0
  148. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/version_check.py +0 -0
  149. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/views/__init__.py +0 -0
  150. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/views/authentication_views.py +0 -0
  151. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/views/fields.py +0 -0
  152. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/views/filters_views.py +0 -0
  153. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/views/locale_views.py +0 -0
  154. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/views/media_rest_view_mixin.py +0 -0
  155. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/views/mfa_authentication_views.py +0 -0
  156. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/views/ordered_rest_view_mixin.py +0 -0
  157. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/views/parameters/__init__.py +0 -0
  158. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/views/parameters/date_format.py +0 -0
  159. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/views/parameters/groups.py +0 -0
  160. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/views/parameters/list_count.py +0 -0
  161. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/views/parameters/list_items.py +0 -0
  162. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/views/parameters/list_mode.py +0 -0
  163. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/views/parameters/list_order.py +0 -0
  164. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/views/parameters/list_search.py +0 -0
  165. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/views/parameters/media_redirect.py +0 -0
  166. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/views/parameters/meta_fields.py +0 -0
  167. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/views/parameters/meta_filters.py +0 -0
  168. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/views/parameters/meta_orders.py +0 -0
  169. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/views/parameters/subset.py +0 -0
  170. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/views/parameters/subset_limit.py +0 -0
  171. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/views/parameters/subset_offset.py +0 -0
  172. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/views/parameters/subset_page.py +0 -0
  173. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/views/parameters/subset_page_size.py +0 -0
  174. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/views/parameters/subset_page_subset.py +0 -0
  175. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/pfxcore/views/rest_views.py +0 -0
  176. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/settings/__init__.py +0 -0
  177. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/settings/dev.py +0 -0
  178. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/userjobs/__init__.py +0 -0
  179. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/userjobs/apps.py +0 -0
  180. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/userjobs/locale/fr/LC_MESSAGES/django.mo +0 -0
  181. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/userjobs/locale/fr/LC_MESSAGES/django.po +0 -0
  182. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/userjobs/migrations/0001_initial.py +0 -0
  183. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/userjobs/migrations/0002_alter_userjob_options_alter_userjob_auto_queued_and_more.py +0 -0
  184. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/userjobs/migrations/0003_clean_scheduled_tasks.py +0 -0
  185. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/userjobs/migrations/0004_userjob_summary_url.py +0 -0
  186. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/userjobs/migrations/0005_swappable_user_job.py +0 -0
  187. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/userjobs/migrations/0006_lowercase_verbose_names.py +0 -0
  188. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/userjobs/migrations/__init__.py +0 -0
  189. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/userjobs/models/__init__.py +0 -0
  190. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/userjobs/models/user_job.py +0 -0
  191. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/userjobs/models/user_job_attachment.py +0 -0
  192. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/userjobs/providers/__init__.py +0 -0
  193. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/userjobs/providers/user_job_django_q_executor.py +0 -0
  194. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/userjobs/providers/user_job_websocket_channel_provider.py +0 -0
  195. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/userjobs/tasks.py +0 -0
  196. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/userjobs/urls.py +0 -0
  197. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/userjobs/views/__init__.py +0 -0
  198. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/userjobs/views/user_job_attachment_view.py +0 -0
  199. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/userjobs/views/user_job_view.py +0 -0
  200. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pfx/userjobs/views/user_websocket_consumer.py +0 -0
  201. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/pyproject.toml +0 -0
  202. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/requirements.txt +0 -0
  203. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/serve-doc +0 -0
  204. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/setup.cfg +0 -0
  205. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/setup.py +0 -0
  206. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/__init__.py +0 -0
  207. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/apps.py +0 -0
  208. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/channel_group_send_calls.py +0 -0
  209. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/locale/fr/LC_MESSAGES/django.po +0 -0
  210. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/mfa_urls.py +0 -0
  211. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/migrations/0001_initial.py +0 -0
  212. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/migrations/__init__.py +0 -0
  213. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/models.py +0 -0
  214. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/settings/__init__.py +0 -0
  215. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/settings/ci.py +0 -0
  216. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/settings/common.py +0 -0
  217. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/settings/dev.py +0 -0
  218. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/settings/dev_custom_example.py +0 -0
  219. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/settings/dev_default.py +0 -0
  220. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/__init__.py +0 -0
  221. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/basic_api_errors.py +0 -0
  222. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/mfa_test_utils.py +0 -0
  223. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_api_doc.py +0 -0
  224. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_api_doc_search.py +0 -0
  225. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_auth_api.py +0 -0
  226. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_body_mixin.py +0 -0
  227. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_cache.py +0 -0
  228. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_client.py +0 -0
  229. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_emails_email_api.py +0 -0
  230. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_emails_email_model.py +0 -0
  231. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_emails_q_mailjet_email_backend.py +0 -0
  232. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_emails_send_email_message.py +0 -0
  233. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_fields_choices.py +0 -0
  234. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_fields_date.py +0 -0
  235. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_fields_decimal.py +0 -0
  236. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_fields_help_text.py +0 -0
  237. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_fields_minutes_duration.py +0 -0
  238. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_fields_one2many.py +0 -0
  239. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_fields_rich_text.py +0 -0
  240. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_fields_time.py +0 -0
  241. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_filters.py +0 -0
  242. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_locale_api.py +0 -0
  243. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_mfa_legacy.py +0 -0
  244. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_mfa_login_api.py +0 -0
  245. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_mfa_setup_api.py +0 -0
  246. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_ordered_rest_view_mixin.py +0 -0
  247. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_perm_tests.py +0 -0
  248. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_permissions.py +0 -0
  249. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_perms_api.py +0 -0
  250. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_post_migrate_groups_update.py +0 -0
  251. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_profiling_middleware.py +0 -0
  252. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_settings.py +0 -0
  253. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_shortcuts.py +0 -0
  254. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_timezone_middleware.py +0 -0
  255. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_tools.py +0 -0
  256. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_user_job_api.py +0 -0
  257. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_user_job_messages.py +0 -0
  258. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_user_job_model.py +0 -0
  259. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_user_queryset.py +0 -0
  260. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_user_websocket.py +0 -0
  261. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_verbose_names.py +0 -0
  262. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_version_check.py +0 -0
  263. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_view_decorators.py +0 -0
  264. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_view_fields.py +0 -0
  265. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/tests/test_ws_cookie_middleware.py +0 -0
  266. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/urls.py +0 -0
  267. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests/views.py +0 -0
  268. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_base_user/__init__.py +0 -0
  269. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_base_user/migrations/__init__.py +0 -0
  270. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_base_user/settings/__init__.py +0 -0
  271. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_base_user/settings/ci.py +0 -0
  272. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_base_user/settings/common.py +0 -0
  273. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_base_user/settings/dev.py +0 -0
  274. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_base_user/settings/dev_custom_example.py +0 -0
  275. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_base_user/settings/dev_default.py +0 -0
  276. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_base_user/tests/__init__.py +0 -0
  277. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_base_user/tests/test_api.py +0 -0
  278. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_base_user/tests/test_auth_api.py +0 -0
  279. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_base_user/urls.py +0 -0
  280. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_base_user/views.py +0 -0
  281. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_custom_user/__init__.py +0 -0
  282. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_custom_user/migrations/0001_initial.py +0 -0
  283. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_custom_user/migrations/__init__.py +0 -0
  284. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_custom_user/models.py +0 -0
  285. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_custom_user/settings/__init__.py +0 -0
  286. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_custom_user/settings/ci.py +0 -0
  287. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_custom_user/settings/common.py +0 -0
  288. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_custom_user/settings/dev.py +0 -0
  289. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_custom_user/settings/dev_custom_example.py +0 -0
  290. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_custom_user/settings/dev_default.py +0 -0
  291. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_custom_user/tests/__init__.py +0 -0
  292. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_custom_user/tests/test_api.py +0 -0
  293. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_custom_user/tests/test_auth_api.py +0 -0
  294. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_custom_user/urls.py +0 -0
  295. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_custom_user/views.py +0 -0
  296. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_custom_user_job/__init__.py +0 -0
  297. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_custom_user_job/migrations/0001_initial.py +0 -0
  298. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_custom_user_job/migrations/0002_lowercase_verbose_names.py +0 -0
  299. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_custom_user_job/migrations/__init__.py +0 -0
  300. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_custom_user_job/models.py +0 -0
  301. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_custom_user_job/providers.py +0 -0
  302. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_custom_user_job/scope.py +0 -0
  303. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_custom_user_job/settings/__init__.py +0 -0
  304. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_custom_user_job/settings/ci.py +0 -0
  305. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_custom_user_job/settings/common.py +0 -0
  306. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_custom_user_job/settings/dev.py +0 -0
  307. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_custom_user_job/settings/dev_custom_example.py +0 -0
  308. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_custom_user_job/settings/dev_default.py +0 -0
  309. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_custom_user_job/tests/__init__.py +0 -0
  310. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_custom_user_job/tests/test_user_job.py +0 -0
  311. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_custom_user_job/urls.py +0 -0
  312. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_user_email_login/__init__.py +0 -0
  313. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_user_email_login/migrations/0001_initial.py +0 -0
  314. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_user_email_login/migrations/__init__.py +0 -0
  315. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_user_email_login/models.py +0 -0
  316. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_user_email_login/settings/__init__.py +0 -0
  317. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_user_email_login/settings/ci.py +0 -0
  318. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_user_email_login/settings/common.py +0 -0
  319. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_user_email_login/settings/dev.py +0 -0
  320. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_user_email_login/settings/dev_custom_example.py +0 -0
  321. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_user_email_login/settings/dev_default.py +0 -0
  322. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_user_email_login/tests/__init__.py +0 -0
  323. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_user_email_login/tests/test_api.py +0 -0
  324. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_user_email_login/tests/test_auth_api.py +0 -0
  325. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_user_email_login/urls.py +0 -0
  326. {django_pfx-1.7.6.dev21 → django_pfx-1.7.6.dev25}/tests_user_email_login/views.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-pfx
3
- Version: 1.7.6.dev21
3
+ Version: 1.7.6.dev25
4
4
  Summary: Django PFX is a toolkit designed to streamline the development of RESTful APIs using the Django framework.
5
5
  Author: Hervé Martinet
6
6
  Author-email: herve.martinet@gmail.com
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-pfx
3
- Version: 1.7.6.dev21
3
+ Version: 1.7.6.dev25
4
4
  Summary: Django PFX is a toolkit designed to streamline the development of RESTful APIs using the Django framework.
5
5
  Author: Hervé Martinet
6
6
  Author-email: herve.martinet@gmail.com
@@ -41,8 +41,10 @@ pfx/emails/apps.py
41
41
  pfx/emails/exceptions.py
42
42
  pfx/emails/shortcuts.py
43
43
  pfx/emails/tasks.py
44
+ pfx/emails/test.py
44
45
  pfx/emails/urls.py
45
46
  pfx/emails/email_backend/__init__.py
47
+ pfx/emails/email_backend/memory_email_backend.py
46
48
  pfx/emails/email_backend/q_mailjet_email_backend.py
47
49
  pfx/emails/locale/fr/LC_MESSAGES/django.mo
48
50
  pfx/emails/locale/fr/LC_MESSAGES/django.po
@@ -73,6 +75,7 @@ pfx/pfxcore/shortcuts.py
73
75
  pfx/pfxcore/test.py
74
76
  pfx/pfxcore/urls.py
75
77
  pfx/pfxcore/version_check.py
78
+ pfx/pfxcore/xml_test_runner.py
76
79
  pfx/pfxcore/apidoc/__init__.py
77
80
  pfx/pfxcore/apidoc/parameters.py
78
81
  pfx/pfxcore/apidoc/schema.py
@@ -133,6 +136,7 @@ pfx/pfxcore/sms/__init__.py
133
136
  pfx/pfxcore/sms/backends/__init__.py
134
137
  pfx/pfxcore/sms/backends/base.py
135
138
  pfx/pfxcore/sms/backends/console.py
139
+ pfx/pfxcore/sms/backends/memory.py
136
140
  pfx/pfxcore/storage/__init__.py
137
141
  pfx/pfxcore/storage/exceptions.py
138
142
  pfx/pfxcore/storage/local_storage.py
@@ -225,6 +229,7 @@ tests/tests/test_emails_email_api.py
225
229
  tests/tests/test_emails_email_model.py
226
230
  tests/tests/test_emails_q_mailjet_email_backend.py
227
231
  tests/tests/test_emails_send_email_message.py
232
+ tests/tests/test_emails_test_tools.py
228
233
  tests/tests/test_fields_choices.py
229
234
  tests/tests/test_fields_date.py
230
235
  tests/tests/test_fields_decimal.py
@@ -246,6 +251,7 @@ tests/tests/test_post_migrate_groups_update.py
246
251
  tests/tests/test_profiling_middleware.py
247
252
  tests/tests/test_settings.py
248
253
  tests/tests/test_shortcuts.py
254
+ tests/tests/test_test_runner.py
249
255
  tests/tests/test_timezone_middleware.py
250
256
  tests/tests/test_tools.py
251
257
  tests/tests/test_user_job_api.py
@@ -29,6 +29,7 @@
29
29
  "pfx/emails/__init__.py",
30
30
  "pfx/emails/apps.py",
31
31
  "pfx/emails/email_backend/__init__.py",
32
+ "pfx/emails/email_backend/memory_email_backend.py",
32
33
  "pfx/emails/email_backend/q_mailjet_email_backend.py",
33
34
  "pfx/emails/exceptions.py",
34
35
  "pfx/emails/locale/fr/LC_MESSAGES/django.po",
@@ -50,6 +51,7 @@
50
51
  "pfx/emails/templates/emails/base_template.html",
51
52
  "pfx/emails/templates/emails/base_template.txt",
52
53
  "pfx/emails/templates/emails/base_template_subject.txt",
54
+ "pfx/emails/test.py",
53
55
  "pfx/emails/urls.py",
54
56
  "pfx/emails/views/__init__.py",
55
57
  "pfx/emails/views/email_rest_view.py",
@@ -118,6 +120,7 @@
118
120
  "pfx/pfxcore/sms/backends/__init__.py",
119
121
  "pfx/pfxcore/sms/backends/base.py",
120
122
  "pfx/pfxcore/sms/backends/console.py",
123
+ "pfx/pfxcore/sms/backends/memory.py",
121
124
  "pfx/pfxcore/storage/__init__.py",
122
125
  "pfx/pfxcore/storage/exceptions.py",
123
126
  "pfx/pfxcore/storage/local_storage.py",
@@ -158,6 +161,7 @@
158
161
  "pfx/pfxcore/views/parameters/subset_page_size.py",
159
162
  "pfx/pfxcore/views/parameters/subset_page_subset.py",
160
163
  "pfx/pfxcore/views/rest_views.py",
164
+ "pfx/pfxcore/xml_test_runner.py",
161
165
  "pfx/settings/__init__.py",
162
166
  "pfx/settings/dev.py",
163
167
  "pfx/userjobs/__init__.py",
@@ -215,6 +219,7 @@
215
219
  "tests/tests/test_emails_email_model.py",
216
220
  "tests/tests/test_emails_q_mailjet_email_backend.py",
217
221
  "tests/tests/test_emails_send_email_message.py",
222
+ "tests/tests/test_emails_test_tools.py",
218
223
  "tests/tests/test_fields_choices.py",
219
224
  "tests/tests/test_fields_date.py",
220
225
  "tests/tests/test_fields_decimal.py",
@@ -236,6 +241,7 @@
236
241
  "tests/tests/test_profiling_middleware.py",
237
242
  "tests/tests/test_settings.py",
238
243
  "tests/tests/test_shortcuts.py",
244
+ "tests/tests/test_test_runner.py",
239
245
  "tests/tests/test_timezone_middleware.py",
240
246
  "tests/tests/test_tools.py",
241
247
  "tests/tests/test_user_job_api.py",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "tag": "1.7.5",
3
- "distance": 21,
4
- "node": "g98a0193f46175f1dd1f3465750eda84d310bdbfc",
3
+ "distance": 25,
4
+ "node": "gf7233748745b72df513a97dd8d75f55284f00bed",
5
5
  "dirty": false,
6
6
  "branch": "HEAD",
7
7
  "node_date": "2026-10-05"
@@ -183,6 +183,8 @@ removed.
183
183
  * `PFX_SMS_BACKEND`: Dotted path to the SMS backend class (optional, default
184
184
  `'pfx.pfxcore.sms.backends.console.ConsoleSMSBackend'`).
185
185
  Implement {class}`pfx.pfxcore.sms.backends.base.BaseSMSBackend` to integrate a real SMS provider.
186
+ In the tests, `pfx.pfxcore.test.TestRunner` replaces it with
187
+ {class}`pfx.pfxcore.sms.backends.memory.MemorySMSBackend` (see [Testing](testing.md)).
186
188
  * `PFX_TOKEN_OTP_VALIDITY`: Validity for OTP tokens (corresponds to the maximum time to enter
187
189
  an OTP code after logging in with a password) (optional, default `{'minutes': 15}`)
188
190
  * `PFX_HOTP_CODE_VALIDITY`: Validity of HOTP codes in minutes (used to send code by email or SMS) (optional, default `15`).
@@ -340,3 +340,93 @@ class BookPermsAPITest(TestPermsAssertMixin):
340
340
  post=403, put=403, delete=403),
341
341
  }
342
342
  ```
343
+
344
+ ## Test runner
345
+ `pfx.pfxcore.test.TestRunner` replaces the SMS and email backends for the
346
+ whole run, parallel workers included: nothing leaves the tests, whatever
347
+ the local settings contain, and what would be sent is kept for the
348
+ assertions.
349
+
350
+ ```python
351
+ TEST_RUNNER = 'pfx.pfxcore.test.TestRunner'
352
+ ```
353
+
354
+ - `PFX_SMS_BACKEND` becomes
355
+ `pfx.pfxcore.sms.backends.memory.MemorySMSBackend`, which collects the
356
+ SMS in memory.
357
+ - If `pfx.emails` is installed, `EMAIL_BACKEND` becomes
358
+ `pfx.emails.email_backend.MemoryEmailBackend` instead of the `locmem`
359
+ backend set by Django. `locmem` puts the emails in
360
+ `django.core.mail.outbox`: the email model is never built nor validated,
361
+ and a sending path that would create an invalid email goes unnoticed.
362
+ `MemoryEmailBackend` builds, validates and saves the email as
363
+ `QMailjetEmailBackend` does, with the `manual` send mode: nothing is
364
+ sent.
365
+
366
+ A project without `pfx.emails` can use the runner as well: only the SMS
367
+ backend is replaced, and the emails stay in Django's `locmem` outbox.
368
+
369
+ To use subclasses of these backends (to fill extra fields of your email
370
+ model, for instance):
371
+ ```python
372
+ PFX_TEST_SMS_BACKEND = 'myapp.tests.MyMemorySMSBackend'
373
+ PFX_TEST_EMAIL_BACKEND = 'myapp.tests.MyMemoryEmailBackend'
374
+ ```
375
+
376
+ For the XML reports of `unittest-xml-reporting` (to install in the
377
+ project), use the XML variant:
378
+ ```python
379
+ TEST_RUNNER = 'pfx.pfxcore.xml_test_runner.XMLTestRunner'
380
+ ```
381
+
382
+ With another runner, combine it with `MemoryBackendsRunnerMixin`:
383
+ ```python
384
+ from pfx.pfxcore.test import MemoryBackendsRunnerMixin
385
+
386
+
387
+ class MyTestRunner(MemoryBackendsRunnerMixin, OtherTestRunner):
388
+ pass
389
+ ```
390
+
391
+ A test class that needs another backend keeps its
392
+ `override_settings(EMAIL_BACKEND=…)` or `override_settings(PFX_SMS_BACKEND=…)`.
393
+
394
+ ## SMSTestMixin
395
+ Helpers to check the SMS of a test. The outbox is emptied before each
396
+ test. They fail if the SMS backend is not a `MemorySMSBackend`, rather
397
+ than reporting an empty outbox.
398
+
399
+ - `sms_outbox()`: the SMS sent so far, as `(to, message)` tuples.
400
+ - `clear_sms_outbox()`: forget the SMS sent so far.
401
+
402
+ ## EmailTestMixin
403
+ `pfx.emails.test.EmailTestMixin` gives helpers to check the emails of a
404
+ test. They fail if the emails are not saved in the email model (with
405
+ `locmem`, for instance), rather than reporting an empty outbox.
406
+
407
+ - `outbox()`: the emails created so far, in creation order.
408
+ - `clear_outbox()`: delete the emails created so far.
409
+ - `email_to(email)`: the addresses of the `To`, without the names.
410
+ - `get_email_html(email)`: the HTML part, fails if there is none.
411
+
412
+ Example :
413
+ ```python
414
+ from django.test import TestCase
415
+
416
+ from pfx.emails.test import EmailTestMixin
417
+ from pfx.pfxcore.test import SMSTestMixin
418
+
419
+
420
+ class ATestClass(EmailTestMixin, SMSTestMixin, TestCase):
421
+
422
+ def a_test(self):
423
+ # … something that sends an email to jrr@example.com
424
+ [email] = self.outbox()
425
+ self.assertEqual(self.email_to(email), ['jrr@example.com'])
426
+ self.assertIn('Welcome', self.get_email_html(email))
427
+
428
+ def another_test(self):
429
+ # … something that sends an SMS
430
+ [(to, message)] = self.sms_outbox()
431
+ self.assertEqual(to, '+41791234567')
432
+ ```
@@ -1 +1,2 @@
1
+ from .memory_email_backend import MemoryEmailBackend
1
2
  from .q_mailjet_email_backend import QMailjetEmailBackend
@@ -0,0 +1,14 @@
1
+ from .q_mailjet_email_backend import QMailjetEmailBackend
2
+
3
+
4
+ class MemoryEmailBackend(QMailjetEmailBackend):
5
+ """Create the emails as :class:`QMailjetEmailBackend` does, but never
6
+ send them.
7
+
8
+ The email model is built, validated and saved, so a sending path that
9
+ would create an invalid email fails. The ``manual`` send mode stops
10
+ there: nothing is queued nor sent to Mailjet."""
11
+
12
+ def create_mail(self, message):
13
+ message.send_mode = 'manual'
14
+ super().create_mail(message)
@@ -0,0 +1,40 @@
1
+ from django.conf import settings
2
+ from django.utils.module_loading import import_string
3
+
4
+ from pfx.emails.email_backend import QMailjetEmailBackend
5
+ from pfx.emails.models import get_email_model
6
+
7
+
8
+ class EmailTestMixin:
9
+ """Helpers to check the emails created by a test."""
10
+
11
+ @classmethod
12
+ def _check_email_backend(cls):
13
+ backend = import_string(settings.EMAIL_BACKEND)
14
+ if not issubclass(backend, QMailjetEmailBackend):
15
+ raise AssertionError(
16
+ f"EMAIL_BACKEND is {settings.EMAIL_BACKEND}, the emails are "
17
+ f"not saved in the email model. Run the tests with a runner "
18
+ f"using pfx.pfxcore.test.MemoryBackendsRunnerMixin.")
19
+
20
+ @classmethod
21
+ def outbox(cls):
22
+ """The emails created so far, in creation order."""
23
+ cls._check_email_backend()
24
+ return list(get_email_model().objects.order_by('pk'))
25
+
26
+ @classmethod
27
+ def clear_outbox(cls):
28
+ cls._check_email_backend()
29
+ get_email_model().objects.all().delete()
30
+
31
+ @classmethod
32
+ def email_to(cls, email):
33
+ """The addresses of the ``To`` of ``email``, without the names."""
34
+ return [r['Email'] for r in email.mail_to]
35
+
36
+ @classmethod
37
+ def get_email_html(cls, email):
38
+ if not email.html_part:
39
+ raise AssertionError("Email does not contain an HTML part.")
40
+ return email.html_part
@@ -1,7 +1,9 @@
1
+ import base64
2
+ import binascii
1
3
  import io
2
4
  import logging
3
5
  from importlib import import_module
4
- from urllib.request import urlopen
6
+ from urllib.parse import unquote_to_bytes
5
7
 
6
8
  from django.core.exceptions import ValidationError
7
9
  from django.db import models
@@ -15,6 +17,30 @@ logger = logging.getLogger(__name__)
15
17
  DEFAULT_KEYS = {"key", "name", "content-type", "content-length"}
16
18
 
17
19
 
20
+ def decode_data_uri(value):
21
+ """Return the bytes carried by a ``data:`` URI, and only that.
22
+
23
+ The uploaded content reaches the server as this string. It used to be
24
+ handed to ``urlopen``, which also opens ``file://`` and ``http://``:
25
+ a client could then have the server read a local file or reach the
26
+ internal network. Only the ``data:`` scheme is accepted here, and it
27
+ is decoded in place, never opened.
28
+
29
+ :raises ValidationError: when the value is not a valid ``data:`` URI
30
+ """
31
+ if not isinstance(value, str) or not value.startswith('data:'):
32
+ raise ValidationError("Le contenu doit être une URI « data: ».")
33
+ header, sep, data = value[len('data:'):].partition(',')
34
+ if not sep:
35
+ raise ValidationError("URI « data: » invalide.")
36
+ if ';base64' in header:
37
+ try:
38
+ return base64.b64decode(data, validate=True)
39
+ except (binascii.Error, ValueError):
40
+ raise ValidationError("URI « data: » invalide.")
41
+ return unquote_to_bytes(data)
42
+
43
+
18
44
  def get_storage_class(class_path):
19
45
  ps = class_path.split('.')
20
46
  return getattr(import_module('.'.join(ps[:-1])), ps[-1])()
@@ -91,8 +117,10 @@ class MediaField(models.JSONField):
91
117
  after_save.append(lambda: self.delete(self._db_value))
92
118
  setattr(obj, self.name, None)
93
119
  if b64:
94
- with urlopen(b64) as response:
95
- file = response.read()
120
+ try:
121
+ file = decode_data_uri(b64)
122
+ except ValidationError as e:
123
+ raise ValidationError({self.name: e.messages})
96
124
  setattr(obj, self.name, dict(key=self.get_key(obj, name)))
97
125
  after_save.append(lambda: save_file(obj, file, name))
98
126
  return after_save
@@ -0,0 +1,15 @@
1
+ from .base import BaseSMSBackend
2
+
3
+
4
+ class MemorySMSBackend(BaseSMSBackend):
5
+ """Collect the SMS in :attr:`outbox` instead of sending them.
6
+
7
+ A new backend instance is built for each send (see
8
+ :func:`pfx.pfxcore.sms.get_sms_backend`), so the outbox is a class
9
+ attribute."""
10
+
11
+ #: The sent SMS, as ``(to, message)`` tuples.
12
+ outbox = []
13
+
14
+ def send_sms(self, to, message):
15
+ self.outbox.append((to, message))
@@ -5,8 +5,16 @@ from functools import reduce
5
5
  from http.cookies import SimpleCookie
6
6
  from json import JSONDecodeError
7
7
 
8
+ from django.apps import apps
8
9
  from django.db import transaction
10
+ from django.test import override_settings
9
11
  from django.test.client import Client as DjangoClient
12
+ from django.test.runner import DiscoverRunner, ParallelTestSuite
13
+ from django.test.runner import _init_worker as django_init_worker
14
+ from django.utils.module_loading import import_string
15
+
16
+ from pfx.pfxcore.settings import settings
17
+ from pfx.pfxcore.sms.backends.memory import MemorySMSBackend
10
18
 
11
19
  logger = logging.getLogger(__name__)
12
20
 
@@ -322,3 +330,94 @@ class MockBoto3Client:
322
330
 
323
331
  def delete_object(self, *args, **kwargs):
324
332
  pass
333
+
334
+
335
+ def get_test_backends():
336
+ """The backends that replace the real ones during the tests."""
337
+ backends = dict(PFX_SMS_BACKEND=getattr(
338
+ settings, 'PFX_TEST_SMS_BACKEND',
339
+ 'pfx.pfxcore.sms.backends.memory.MemorySMSBackend'))
340
+ if apps.is_installed('pfx.emails'):
341
+ backends['EMAIL_BACKEND'] = getattr(
342
+ settings, 'PFX_TEST_EMAIL_BACKEND',
343
+ 'pfx.emails.email_backend.MemoryEmailBackend')
344
+ return backends
345
+
346
+
347
+ def _init_worker(*args, **kwargs):
348
+ # A spawned worker calls setup_test_environment() again, which puts
349
+ # locmem back.
350
+ django_init_worker(*args, **kwargs)
351
+ override_settings(**get_test_backends()).enable()
352
+
353
+
354
+ class MemoryBackendsParallelTestSuite(ParallelTestSuite):
355
+ init_worker = _init_worker
356
+
357
+
358
+ class MemoryBackendsRunnerMixin:
359
+ """Replace the SMS and email backends for the whole run.
360
+
361
+ Nothing leaves the tests, whatever the local settings contain, and
362
+ what would be sent is kept for the assertions (see
363
+ :class:`SMSTestMixin` and :class:`pfx.emails.test.EmailTestMixin`).
364
+
365
+ - ``PFX_SMS_BACKEND`` becomes ``PFX_TEST_SMS_BACKEND`` (default
366
+ :class:`pfx.pfxcore.sms.backends.memory.MemorySMSBackend`).
367
+ - If ``pfx.emails`` is installed, ``EMAIL_BACKEND`` becomes
368
+ ``PFX_TEST_EMAIL_BACKEND`` (default
369
+ :class:`pfx.emails.email_backend.MemoryEmailBackend`) instead of
370
+ the ``locmem`` backend set by Django: the email model is built and
371
+ validated, which ``locmem`` never does.
372
+
373
+ A test class decorated with ``override_settings`` keeps its own
374
+ backends."""
375
+
376
+ parallel_test_suite = MemoryBackendsParallelTestSuite
377
+
378
+ def setup_test_environment(self, **kwargs):
379
+ super().setup_test_environment(**kwargs)
380
+ self._test_backends = override_settings(**get_test_backends())
381
+ self._test_backends.enable()
382
+
383
+ def teardown_test_environment(self, **kwargs):
384
+ self._test_backends.disable()
385
+ super().teardown_test_environment(**kwargs)
386
+
387
+
388
+ class TestRunner(MemoryBackendsRunnerMixin, DiscoverRunner):
389
+ pass
390
+
391
+
392
+ class SMSTestMixin:
393
+ """Helpers to check the SMS sent by a test.
394
+
395
+ The outbox is emptied before each test, as Django does for
396
+ ``mail.outbox``."""
397
+
398
+ @classmethod
399
+ def _sms_backend(cls):
400
+ backend = import_string(settings.PFX_SMS_BACKEND)
401
+ if not issubclass(backend, MemorySMSBackend):
402
+ raise AssertionError(
403
+ f"PFX_SMS_BACKEND is {settings.PFX_SMS_BACKEND}, the SMS "
404
+ f"are not collected. Run the tests with a runner using "
405
+ f"pfx.pfxcore.test.MemoryBackendsRunnerMixin.")
406
+ return backend
407
+
408
+ # __call__ rather than _pre_setup: a classmethod in Django 5.2, an
409
+ # instance method in 4.2.
410
+ def __call__(self, *args, **kwargs):
411
+ backend = import_string(settings.PFX_SMS_BACKEND)
412
+ if issubclass(backend, MemorySMSBackend):
413
+ backend.outbox.clear()
414
+ return super().__call__(*args, **kwargs)
415
+
416
+ @classmethod
417
+ def sms_outbox(cls):
418
+ """The SMS sent so far, as ``(to, message)`` tuples."""
419
+ return list(cls._sms_backend().outbox)
420
+
421
+ @classmethod
422
+ def clear_sms_outbox(cls):
423
+ cls._sms_backend().outbox.clear()
@@ -0,0 +1,8 @@
1
+ from xmlrunner.extra.djangotestrunner import XMLTestRunner as BaseXMLTestRunner
2
+
3
+ from pfx.pfxcore.test import MemoryBackendsRunnerMixin
4
+
5
+
6
+ class XMLTestRunner(MemoryBackendsRunnerMixin, BaseXMLTestRunner):
7
+ """:class:`pfx.pfxcore.test.TestRunner` with the XML reports of
8
+ ``unittest-xml-reporting``, which must be installed."""
@@ -847,6 +847,37 @@ class BasicAPITest(TestAssertMixin, TestCase):
847
847
  self.assertJE(response, 'local_file', None)
848
848
  self.assertFalse(Path(f'/tmp/django-pfx-filestore/{key2}').exists())
849
849
 
850
+ def test_create_file_rejects_non_data_uri(self):
851
+ """A media upload only accepts a ``data:`` URI.
852
+
853
+ The content used to be handed to ``urlopen``, so ``file://`` made
854
+ the server read a local file and ``http://`` reach the network.
855
+ Both are refused now, with a field error, before anything is read.
856
+ """
857
+ for uri in (
858
+ 'file:///etc/hostname',
859
+ 'http://169.254.169.254/latest/meta-data/',
860
+ 'not a uri at all'):
861
+ response = self.client.post('/api/books', dict(
862
+ name="Test Book",
863
+ author=self.author1.pk,
864
+ pub_date='2000-01-01',
865
+ local_file=dict(name='x.txt', base64=uri)))
866
+ self.assertRC(response, 422)
867
+ self.assertJEExists(response, 'local_file')
868
+
869
+ def test_create_file_url_encoded_data_uri(self):
870
+ """A ``data:`` URI without ``;base64`` is percent-decoded, as
871
+ ``urlopen`` did, so that path keeps working without it."""
872
+ response = self.client.post('/api/books', dict(
873
+ name="Test Book",
874
+ author=self.author1.pk,
875
+ pub_date='2000-01-01',
876
+ local_file=dict(
877
+ name='test.txt', base64='data:text/plain,Hello%20world')))
878
+ self.assertRC(response, 200)
879
+ self.assertJE(response, 'local_file.content-length', '11')
880
+
850
881
  def test_get_file_content_disposition(self):
851
882
  """The file name must survive both local serving paths.
852
883
 
@@ -0,0 +1,60 @@
1
+ from django.core import mail
2
+ from django.core.exceptions import ValidationError
3
+ from django.core.mail import EmailMultiAlternatives, send_mail
4
+ from django.test import TestCase, override_settings
5
+
6
+ from pfx.emails.models import Email
7
+ from pfx.emails.test import EmailTestMixin
8
+
9
+ MEMORY_BACKEND = 'pfx.emails.email_backend.MemoryEmailBackend'
10
+
11
+
12
+ @override_settings(EMAIL_BACKEND=MEMORY_BACKEND)
13
+ class TestMemoryEmailBackend(EmailTestMixin, TestCase):
14
+ """The backend goes as far as the saved email, never further."""
15
+
16
+ def test_the_email_is_saved_and_not_sent(self):
17
+ send_mail(
18
+ 'Subject', 'Content', 'from@example.com', ['to@example.com'])
19
+ email = Email.objects.get()
20
+ self.assertEqual(email.send_mode, 'manual')
21
+ self.assertEqual(email.status, 'to_send')
22
+ self.assertEqual(mail.outbox, [])
23
+
24
+ def test_an_invalid_email_fails(self):
25
+ """What locmem cannot see: the email model refuses the message."""
26
+ with self.assertRaises(ValidationError):
27
+ send_mail(
28
+ 'Subject', 'Content', 'from@example.com', ['not-an-email'])
29
+
30
+ def test_outbox_helpers(self):
31
+ send_mail(
32
+ 'First', 'Content', 'from@example.com',
33
+ ['"Jean Dupont" <jean@example.com>', 'paul@example.com'])
34
+ message = EmailMultiAlternatives(
35
+ 'Second', 'Text', 'from@example.com', ['marie@example.com'])
36
+ message.attach_alternative('<p>HTML</p>', 'text/html')
37
+ message.send()
38
+
39
+ first, second = self.outbox()
40
+ self.assertEqual(
41
+ [first.subject, second.subject], ['First', 'Second'])
42
+ self.assertEqual(
43
+ self.email_to(first), ['jean@example.com', 'paul@example.com'])
44
+ self.assertEqual(self.get_email_html(second), '<p>HTML</p>')
45
+ with self.assertRaises(AssertionError):
46
+ self.get_email_html(first)
47
+
48
+ self.clear_outbox()
49
+ self.assertEqual(self.outbox(), [])
50
+
51
+
52
+ class TestEmailTestMixinWithLocmem(EmailTestMixin, TestCase):
53
+ """Without the runner, locmem is still there: the helpers refuse to
54
+ answer instead of reporting an empty outbox."""
55
+
56
+ def test_the_helpers_refuse_locmem(self):
57
+ with self.assertRaises(AssertionError):
58
+ self.outbox()
59
+ with self.assertRaises(AssertionError):
60
+ self.clear_outbox()