django-pfx 1.7.2.dev12__tar.gz → 1.7.2.dev16__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 (207) hide show
  1. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/PKG-INFO +1 -1
  2. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/django_pfx.egg-info/PKG-INFO +1 -1
  3. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/django_pfx.egg-info/SOURCES.txt +2 -0
  4. django_pfx-1.7.2.dev16/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
  5. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.po +38 -24
  6. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/models/mfa_user_mixin.py +43 -9
  7. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/authentication_views.py +13 -14
  8. django_pfx-1.7.2.dev16/tests/migrations/0005_mfausermixin_fields_fix.py +17 -0
  9. django_pfx-1.7.2.dev16/tests/migrations/0006_rename_otp_enabled_user_mfa_authenticator_enabled_and_more.py +29 -0
  10. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/__init__.py +1 -1
  11. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_auth_api.py +24 -22
  12. django_pfx-1.7.2.dev12/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
  13. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/.gitignore +0 -0
  14. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/.gitlab-ci.yml +0 -0
  15. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/.pre-commit-config.yaml +0 -0
  16. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/LICENSE +0 -0
  17. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/MANIFEST.in +0 -0
  18. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/README.md +0 -0
  19. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/django_pfx.egg-info/dependency_links.txt +0 -0
  20. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/django_pfx.egg-info/requires.txt +0 -0
  21. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/django_pfx.egg-info/top_level.txt +0 -0
  22. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/doc/Makefile +0 -0
  23. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/doc/conf.py +0 -0
  24. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/doc/index.rst +0 -0
  25. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/doc/source/api.views.rst +0 -0
  26. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/doc/source/authentication.md +0 -0
  27. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/doc/source/decorator.md +0 -0
  28. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/doc/source/generate_openapi.md +0 -0
  29. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/doc/source/getting_started.md +0 -0
  30. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/doc/source/internationalisation.md +0 -0
  31. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/doc/source/model.md +0 -0
  32. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/doc/source/pfx_views.md +0 -0
  33. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/doc/source/profiling.md +0 -0
  34. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/doc/source/settings.md +0 -0
  35. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/doc/source/testing.md +0 -0
  36. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/img/pfx.png +0 -0
  37. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/img/pfx.svg +0 -0
  38. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/make_messages +0 -0
  39. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/manage.py +0 -0
  40. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/__init__.py +0 -0
  41. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/__init__.py +0 -0
  42. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/apidoc/__init__.py +0 -0
  43. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/apidoc/parameters.py +0 -0
  44. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/apidoc/schema.py +0 -0
  45. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/apidoc/tags.py +0 -0
  46. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/apps.py +0 -0
  47. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/decorator/__init__.py +0 -0
  48. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/decorator/rest.py +0 -0
  49. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/default_settings.py +0 -0
  50. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/exceptions.py +0 -0
  51. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/fields/__init__.py +0 -0
  52. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/fields/decimal_field.py +0 -0
  53. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/fields/media_field.py +0 -0
  54. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/fields/minutes_duration_field.py +0 -0
  55. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/fields/rich_text_field.py +0 -0
  56. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/http/__init__.py +0 -0
  57. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/http/json_response.py +0 -0
  58. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/management/__init__.py +0 -0
  59. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/management/commands/__init__.py +0 -0
  60. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/management/commands/makeapidoc.py +0 -0
  61. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/management/commands/profile.py +0 -0
  62. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/middleware/__init__.py +0 -0
  63. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/middleware/authentication.py +0 -0
  64. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/middleware/locale.py +0 -0
  65. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/middleware/profiling.py +0 -0
  66. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/migrations/0001_initial.py +0 -0
  67. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/migrations/0002_pfxpermissionsuser.py +0 -0
  68. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/migrations/0003_delete_pfxpermissionsuser.py +0 -0
  69. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/migrations/0004_alter_loginban_failed_counter_and_more.py +0 -0
  70. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/migrations/__init__.py +0 -0
  71. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/migrations/operations/__init__.py +0 -0
  72. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/migrations/operations/permissions.py +0 -0
  73. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/models/__init__.py +0 -0
  74. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/models/abstract_pfx_base_user.py +0 -0
  75. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/models/cache_mixins.py +0 -0
  76. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/models/login_ban.py +0 -0
  77. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/models/not_null_fields.py +0 -0
  78. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/models/ordered_model_mixin.py +0 -0
  79. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/models/pfx_models.py +0 -0
  80. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/models/pfx_user.py +0 -0
  81. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/models/user_filtered_queryset_mixin.py +0 -0
  82. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/serializers/__init__.py +0 -0
  83. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/serializers/json.py +0 -0
  84. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/settings.py +0 -0
  85. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/shortcuts.py +0 -0
  86. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/sms/__init__.py +0 -0
  87. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/sms/backends/__init__.py +0 -0
  88. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/sms/backends/base.py +0 -0
  89. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/sms/backends/console.py +0 -0
  90. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/storage/__init__.py +0 -0
  91. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/storage/exceptions.py +0 -0
  92. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/storage/local_storage.py +0 -0
  93. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/storage/s3_storage.py +0 -0
  94. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/templates/registration/otp_code_email.txt +0 -0
  95. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/templates/registration/otp_code_subject.txt +0 -0
  96. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/templates/registration/password_reset_email.txt +0 -0
  97. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/templates/registration/password_reset_subject.txt +0 -0
  98. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/templates/registration/welcome_email.txt +0 -0
  99. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/templates/registration/welcome_subject.txt +0 -0
  100. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/test.py +0 -0
  101. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/urls.py +0 -0
  102. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/__init__.py +0 -0
  103. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/fields.py +0 -0
  104. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/filters_views.py +0 -0
  105. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/locale_views.py +0 -0
  106. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/media_rest_view_mixin.py +0 -0
  107. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/ordered_rest_view_mixin.py +0 -0
  108. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/__init__.py +0 -0
  109. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/date_format.py +0 -0
  110. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/groups.py +0 -0
  111. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/list_count.py +0 -0
  112. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/list_items.py +0 -0
  113. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/list_mode.py +0 -0
  114. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/list_order.py +0 -0
  115. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/list_search.py +0 -0
  116. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/media_redirect.py +0 -0
  117. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/meta_fields.py +0 -0
  118. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/meta_filters.py +0 -0
  119. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/meta_orders.py +0 -0
  120. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/subset.py +0 -0
  121. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/subset_limit.py +0 -0
  122. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/subset_offset.py +0 -0
  123. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/subset_page.py +0 -0
  124. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/subset_page_size.py +0 -0
  125. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/parameters/subset_page_subset.py +0 -0
  126. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/pfxcore/views/rest_views.py +0 -0
  127. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/settings/__init__.py +0 -0
  128. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pfx/settings/dev.py +0 -0
  129. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/pyproject.toml +0 -0
  130. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/requirements.txt +0 -0
  131. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/serve-doc +0 -0
  132. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/setup.cfg +0 -0
  133. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/setup.py +0 -0
  134. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/__init__.py +0 -0
  135. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/apps.py +0 -0
  136. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/locale/fr/LC_MESSAGES/django.po +0 -0
  137. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/migrations/0001_initial.py +0 -0
  138. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/migrations/0002_alter_book_cover.py +0 -0
  139. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/migrations/0003_book_local_file.py +0 -0
  140. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/migrations/0004_mfausermixin_fields.py +0 -0
  141. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/migrations/__init__.py +0 -0
  142. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/models.py +0 -0
  143. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/settings/__init__.py +0 -0
  144. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/settings/ci.py +0 -0
  145. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/settings/common.py +0 -0
  146. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/settings/dev.py +0 -0
  147. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/settings/dev_custom_example.py +0 -0
  148. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/settings/dev_default.py +0 -0
  149. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/basic_api_errors.py +0 -0
  150. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/basic_api_test.py +0 -0
  151. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_api_doc.py +0 -0
  152. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_api_doc_search.py +0 -0
  153. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_body_mixin.py +0 -0
  154. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_cache.py +0 -0
  155. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_client.py +0 -0
  156. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_fields_choices.py +0 -0
  157. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_fields_date.py +0 -0
  158. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_fields_decimal.py +0 -0
  159. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_fields_minutes_duration.py +0 -0
  160. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_fields_one2many.py +0 -0
  161. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_fields_rich_text.py +0 -0
  162. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_filters.py +0 -0
  163. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_locale_api.py +0 -0
  164. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_ordered_rest_view_mixin.py +0 -0
  165. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_perm_tests.py +0 -0
  166. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_permissions.py +0 -0
  167. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_perms_api.py +0 -0
  168. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_post_migrate_groups_update.py +0 -0
  169. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_profiling_middleware.py +0 -0
  170. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_settings.py +0 -0
  171. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_shortcuts.py +0 -0
  172. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_timezone_middleware.py +0 -0
  173. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_tools.py +0 -0
  174. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_user_queryset.py +0 -0
  175. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_view_decorators.py +0 -0
  176. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/tests/test_view_fields.py +0 -0
  177. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/urls.py +0 -0
  178. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests/views.py +0 -0
  179. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_base_user/__init__.py +0 -0
  180. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_base_user/migrations/0001_initial.py +0 -0
  181. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_base_user/migrations/__init__.py +0 -0
  182. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_base_user/settings/__init__.py +0 -0
  183. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_base_user/settings/ci.py +0 -0
  184. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_base_user/settings/common.py +0 -0
  185. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_base_user/settings/dev.py +0 -0
  186. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_base_user/settings/dev_custom_example.py +0 -0
  187. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_base_user/settings/dev_default.py +0 -0
  188. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_base_user/tests/__init__.py +0 -0
  189. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_base_user/tests/test_api.py +0 -0
  190. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_base_user/tests/test_auth_api.py +0 -0
  191. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_base_user/urls.py +0 -0
  192. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_base_user/views.py +0 -0
  193. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_custom_user/__init__.py +0 -0
  194. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_custom_user/migrations/0001_initial.py +0 -0
  195. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_custom_user/migrations/__init__.py +0 -0
  196. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_custom_user/models.py +0 -0
  197. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_custom_user/settings/__init__.py +0 -0
  198. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_custom_user/settings/ci.py +0 -0
  199. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_custom_user/settings/common.py +0 -0
  200. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_custom_user/settings/dev.py +0 -0
  201. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_custom_user/settings/dev_custom_example.py +0 -0
  202. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_custom_user/settings/dev_default.py +0 -0
  203. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_custom_user/tests/__init__.py +0 -0
  204. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_custom_user/tests/test_api.py +0 -0
  205. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_custom_user/tests/test_auth_api.py +0 -0
  206. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_custom_user/urls.py +0 -0
  207. {django_pfx-1.7.2.dev12 → django_pfx-1.7.2.dev16}/tests_custom_user/views.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-pfx
3
- Version: 1.7.2.dev12
3
+ Version: 1.7.2.dev16
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.2.dev12
3
+ Version: 1.7.2.dev16
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
@@ -135,6 +135,8 @@ tests/migrations/0001_initial.py
135
135
  tests/migrations/0002_alter_book_cover.py
136
136
  tests/migrations/0003_book_local_file.py
137
137
  tests/migrations/0004_mfausermixin_fields.py
138
+ tests/migrations/0005_mfausermixin_fields_fix.py
139
+ tests/migrations/0006_rename_otp_enabled_user_mfa_authenticator_enabled_and_more.py
138
140
  tests/migrations/__init__.py
139
141
  tests/settings/__init__.py
140
142
  tests/settings/ci.py
@@ -7,7 +7,7 @@ msgid ""
7
7
  msgstr ""
8
8
  "Project-Id-Version: \n"
9
9
  "Report-Msgid-Bugs-To: \n"
10
- "POT-Creation-Date: 2026-04-20 16:17+0200\n"
10
+ "POT-Creation-Date: 2026-04-23 14:57+0200\n"
11
11
  "PO-Revision-Date: 2021-06-22 23:31+0200\n"
12
12
  "Last-Translator: \n"
13
13
  "Language-Team: \n"
@@ -61,11 +61,11 @@ msgstr ""
61
61
 
62
62
  #: models/abstract_pfx_base_user.py:52
63
63
  msgid "user"
64
- msgstr ""
64
+ msgstr "utilisateur"
65
65
 
66
66
  #: models/abstract_pfx_base_user.py:53
67
67
  msgid "users"
68
- msgstr ""
68
+ msgstr "utilisateurs"
69
69
 
70
70
  #: models/login_ban.py:54
71
71
  msgid "Username"
@@ -112,22 +112,32 @@ msgid "Temporary SMS phone number"
112
112
  msgstr "Numéro de téléphone temporaire (SMS)"
113
113
 
114
114
  #: models/mfa_user_mixin.py:35
115
- #, fuzzy
116
- #| msgid "OTP enabled"
117
115
  msgid "SMS MFA enabled"
118
- msgstr "OTP activé"
116
+ msgstr "MFA SMS activé"
119
117
 
120
- #: models/mfa_user_mixin.py:37
118
+ #: models/mfa_user_mixin.py:38
121
119
  msgid "Email MFA enabled"
122
120
  msgstr "MFA par email activé"
123
121
 
124
- #: models/mfa_user_mixin.py:40
122
+ #: models/mfa_user_mixin.py:41
125
123
  msgid "MFA setup dismissed"
126
124
  msgstr "Configuration MFA annulée"
127
125
 
128
- #: models/mfa_user_mixin.py:47 views/authentication_views.py:712
129
- msgid "OTP enabled"
130
- msgstr "OTP activé"
126
+ #: models/mfa_user_mixin.py:44
127
+ msgid "Authenticator MFA enabled"
128
+ msgstr "Application MFA activée"
129
+
130
+ #: models/mfa_user_mixin.py:61
131
+ msgid "MFA enabled"
132
+ msgstr "MFA activé"
133
+
134
+ #: models/mfa_user_mixin.py:68
135
+ msgid "MFA setup required"
136
+ msgstr "Configuration MFA requise"
137
+
138
+ #: models/mfa_user_mixin.py:74
139
+ msgid "Is MFA forced"
140
+ msgstr "MFA est forcé"
131
141
 
132
142
  #: models/pfx_models.py:14
133
143
  #, python-format
@@ -245,51 +255,55 @@ msgstr ""
245
255
  "Votre connexion est temporairement désactivée après plusieurs tentatives "
246
256
  "infructueuses, veuillez réessayer dans {seconds} secondes."
247
257
 
248
- #: views/authentication_views.py:214
258
+ #: views/authentication_views.py:209
249
259
  msgid "Successful login"
250
260
  msgstr "Connexion réussie"
251
261
 
252
- #: views/authentication_views.py:385
262
+ #: views/authentication_views.py:386
253
263
  msgid "This field is required."
254
264
  msgstr "Ce champ est requis."
255
265
 
256
- #: views/authentication_views.py:396
266
+ #: views/authentication_views.py:397
257
267
  msgid "Code sent."
258
268
  msgstr "Code envoyé."
259
269
 
260
- #: views/authentication_views.py:459 views/authentication_views.py:629
270
+ #: views/authentication_views.py:460 views/authentication_views.py:628
261
271
  msgid "password updated successfully"
262
272
  msgstr "le mot de passe a été mis à jour avec succès"
263
273
 
264
- #: views/authentication_views.py:464
274
+ #: views/authentication_views.py:465
265
275
  msgid "Incorrect password"
266
276
  msgstr "Mot de passe incorrect"
267
277
 
268
- #: views/authentication_views.py:467 views/authentication_views.py:638
278
+ #: views/authentication_views.py:468 views/authentication_views.py:637
269
279
  msgid "Empty password is not allowed"
270
280
  msgstr "Un mot de passe vide n’est pas autorisé"
271
281
 
272
- #: views/authentication_views.py:558
282
+ #: views/authentication_views.py:557
273
283
  msgid "User and token are valid"
274
284
  msgstr "L'utilisateur et le token sont valides"
275
285
 
276
- #: views/authentication_views.py:560
286
+ #: views/authentication_views.py:559
277
287
  msgid "User or token is invalid"
278
288
  msgstr "L'utilisateur ou le token est invalide"
279
289
 
280
- #: views/authentication_views.py:672
290
+ #: views/authentication_views.py:671
281
291
  msgid "OTP is already enabled"
282
292
  msgstr "OTP est déjà activé"
283
293
 
284
- #: views/authentication_views.py:713 views/authentication_views.py:749
294
+ #: views/authentication_views.py:711
295
+ msgid "OTP enabled"
296
+ msgstr "OTP activé"
297
+
298
+ #: views/authentication_views.py:712 views/authentication_views.py:748
285
299
  msgid "Invalid code"
286
300
  msgstr "Code invalide"
287
301
 
288
- #: views/authentication_views.py:748
302
+ #: views/authentication_views.py:747
289
303
  msgid "OTP disabled"
290
304
  msgstr "OTP désactivé"
291
305
 
292
- #: views/authentication_views.py:1009
306
+ #: views/authentication_views.py:1008
293
307
  msgid ""
294
308
  "If the email address you entered is correct, you will receive an email from "
295
309
  "us with instructions to reset your password."
@@ -298,7 +312,7 @@ msgstr ""
298
312
  "un courrier électronique de notre part contenant des instructions pour "
299
313
  "réinitialiser votre mot de passe."
300
314
 
301
- #: views/authentication_views.py:1083
315
+ #: views/authentication_views.py:1082
302
316
  msgid "A new authentication code has been sent by email."
303
317
  msgstr "Un nouveau code d'authentification a été envoyé par e-mail."
304
318
 
@@ -32,21 +32,51 @@ class MFAUserMixin(models.Model):
32
32
  sms_phone_number_tmp = models.CharField(
33
33
  _("Temporary SMS phone number"), max_length=32, null=True, blank=True)
34
34
  #: SMS MFA enabled.
35
- sms_enabled = models.BooleanField(_("SMS MFA enabled"), default=False)
35
+ mfa_sms_enabled = models.BooleanField(_("SMS MFA enabled"), default=False)
36
36
  #: Email MFA enabled.
37
- email_enabled = models.BooleanField(_("Email MFA enabled"), default=False)
37
+ mfa_email_enabled = models.BooleanField(
38
+ _("Email MFA enabled"), default=False)
38
39
  #: User has dismissed or completed the MFA setup screen.
39
40
  mfa_setup_dismissed = models.BooleanField(
40
41
  _("MFA setup dismissed"), default=False)
42
+ #: Authenticator app MFA explicitly enabled by the user.
43
+ mfa_authenticator_enabled = models.BooleanField(
44
+ _("Authenticator MFA enabled"), default=False)
41
45
 
42
46
  class Meta:
43
47
  abstract = True
44
48
 
45
- # --- deprecated alias ---
46
-
47
- @rest_property(_("OTP enabled"), "BooleanField")
48
- def is_otp(self):
49
- return bool(self.otp_secret_token)
49
+ def auth_json_repr(self, **kw):
50
+ res = super().auth_json_repr(
51
+ is_mfa=self.is_mfa,
52
+ mfa_authenticator_enabled=self.mfa_authenticator_enabled,
53
+ mfa_sms_enabled=self.mfa_sms_enabled,
54
+ mfa_email_enabled=self.mfa_email_enabled,
55
+ mfa_setup_dismissed=self.mfa_setup_dismissed,
56
+ mfa_setup_required=self.mfa_setup_required,
57
+ mfa_forced=self.mfa_forced,
58
+ **kw)
59
+ return res
60
+
61
+ @rest_property(_("MFA enabled"), "BooleanField")
62
+ def is_mfa(self):
63
+ return (
64
+ self.mfa_authenticator_enabled
65
+ or self.mfa_sms_enabled
66
+ or self.mfa_email_enabled)
67
+
68
+ @rest_property(_("MFA setup required"), "BooleanField")
69
+ def mfa_setup_required(self):
70
+ return (
71
+ self.mfa_forced and
72
+ not self.mfa_setup_dismissed)
73
+
74
+ @rest_property(_("Is MFA forced"), "BooleanField")
75
+ def mfa_forced(self):
76
+ """Return True if MFA is forced for this user.
77
+
78
+ Can be overridden to implement per-organisation logic."""
79
+ return settings.PFX_MFA_FORCE
50
80
 
51
81
  def need_otp_response_extra(self):
52
82
  """Add extra attribute to response when need_otp=True."""
@@ -74,8 +104,10 @@ class MFAUserMixin(models.Model):
74
104
  if self.is_otp_valid(otp_code, tmp=True):
75
105
  self.otp_secret_token = self.otp_secret_token_tmp
76
106
  self.otp_secret_token_tmp = None
107
+ self.mfa_authenticator_enabled = True
77
108
  self.save(update_fields=[
78
- 'otp_secret_token', 'otp_secret_token_tmp'])
109
+ 'otp_secret_token', 'otp_secret_token_tmp',
110
+ 'mfa_authenticator_enabled'])
79
111
  return True
80
112
  return False
81
113
 
@@ -85,7 +117,9 @@ class MFAUserMixin(models.Model):
85
117
  Remove the OTP secret token.
86
118
  """
87
119
  self.otp_secret_token = None
88
- self.save(update_fields=['otp_secret_token'])
120
+ self.mfa_authenticator_enabled = False
121
+ self.save(update_fields=[
122
+ 'otp_secret_token', 'mfa_authenticator_enabled'])
89
123
 
90
124
  def get_otp_setup_uri(self, tmp=False, with_color=True):
91
125
  """Return the setup URL for OTP activation.
@@ -190,10 +190,6 @@ class AuthenticationView(
190
190
  user.last_login = tz.now()
191
191
  user.save(update_fields=['last_login'])
192
192
  token = self._prepare_token(user, mode, remember_me)
193
- mfa_setup_required = (
194
- isinstance(user, MFAUserMixin) and
195
- self.is_mfa_forced(user) and
196
- not user.mfa_setup_dismissed)
197
193
  if mode == 'cookie':
198
194
  if remember_me:
199
195
  expires = datetime.now(
@@ -203,7 +199,6 @@ class AuthenticationView(
203
199
 
204
200
  res = JsonResponse(dict(
205
201
  need_otp=False,
206
- mfa_setup_required=mfa_setup_required,
207
202
  user=self.get_user_information(user)))
208
203
  res.set_cookie(
209
204
  'token', token, secure=settings.PFX_COOKIE_SECURE,
@@ -213,7 +208,6 @@ class AuthenticationView(
213
208
  return JsonResponse(dict(
214
209
  message=message or _("Successful login"),
215
210
  need_otp=False,
216
- mfa_setup_required=mfa_setup_required,
217
211
  token=token,
218
212
  user=self.get_user_information(user)))
219
213
 
@@ -230,6 +224,12 @@ class AuthenticationView(
230
224
  self.send_mfa_challenge(user, backend_id)
231
225
  token = self._prepare_token(user, mode, remember_me, otp_login=True)
232
226
  available = self.get_mfa_backends(user)
227
+ # Ensure the active OOB backend appears in available_backends so the
228
+ # frontend can offer a resend button (e.g. forced-email fallback where
229
+ # email_enabled is False but email is still the active channel).
230
+ available_for_response = list(available)
231
+ if is_oob and backend_id not in available_for_response:
232
+ available_for_response.append(backend_id)
233
233
  extra = user.need_otp_response_extra() if isinstance(
234
234
  user, MFAUserMixin) else {}
235
235
  return JsonResponse(dict(
@@ -239,7 +239,7 @@ class AuthenticationView(
239
239
  mfa_backend={'id': backend_id, 'is_oob': is_oob},
240
240
  available_backends=[
241
241
  {'id': b, 'is_oob': b in {'email', 'sms'}}
242
- for b in available
242
+ for b in available_for_response
243
243
  ]))
244
244
 
245
245
  # --- MFA helpers ---
@@ -253,11 +253,12 @@ class AuthenticationView(
253
253
  return []
254
254
  result = []
255
255
  for backend_id in settings.PFX_MFA_BACKENDS:
256
- if backend_id == 'authenticator' and user.otp_secret_token:
256
+ if (backend_id == 'authenticator'
257
+ and user.mfa_authenticator_enabled):
257
258
  result.append(backend_id)
258
- elif backend_id == 'sms' and user.sms_enabled:
259
+ elif backend_id == 'sms' and user.mfa_sms_enabled:
259
260
  result.append(backend_id)
260
- elif backend_id == 'email' and user.email_enabled:
261
+ elif backend_id == 'email' and user.mfa_email_enabled:
261
262
  result.append(backend_id)
262
263
  return result
263
264
 
@@ -265,7 +266,7 @@ class AuthenticationView(
265
266
  """Return True if MFA is forced for this user.
266
267
 
267
268
  Can be overridden to implement per-organisation logic."""
268
- return settings.PFX_MFA_FORCE
269
+ return isinstance(user, MFAUserMixin) and user.mfa_forced
269
270
 
270
271
  def get_active_mfa_backend(self, user):
271
272
  """Return the highest-priority active MFA backend ID for this user.
@@ -503,8 +504,6 @@ class AuthenticationView(
503
504
 
504
505
  :param user: The user"""
505
506
  info = user.auth_json_repr()
506
- if isinstance(user, MFAUserMixin):
507
- info.update(is_otp=user.is_otp)
508
507
  return info
509
508
 
510
509
  @classmethod
@@ -667,7 +666,7 @@ class AuthenticationView(
667
666
  if not isinstance(self.request.user, MFAUserMixin):
668
667
  logger.error("User must inherit MFAUserMixin to use MFA")
669
668
  raise NotFoundError()
670
- if self.request.user.otp_secret_token:
669
+ if self.request.user.mfa_authenticator_enabled:
671
670
  return JsonResponse(
672
671
  dict(message=_("OTP is already enabled")), status=400)
673
672
  self.request.user.enable_otp()
@@ -0,0 +1,17 @@
1
+ from django.db import migrations, models
2
+
3
+
4
+ class Migration(migrations.Migration):
5
+
6
+ dependencies = [
7
+ ('tests', '0004_mfausermixin_fields'),
8
+ ]
9
+
10
+ operations = [
11
+ migrations.AddField(
12
+ model_name='user',
13
+ name='otp_enabled',
14
+ field=models.BooleanField(
15
+ default=False, verbose_name='Authenticator MFA enabled'),
16
+ ),
17
+ ]
@@ -0,0 +1,29 @@
1
+ # Generated by Django 4.2.30 on 2026-04-23 07:33
2
+ # flake8: noqa
3
+
4
+ from django.db import migrations
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('tests', '0005_mfausermixin_fields_fix'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.RenameField(
15
+ model_name='user',
16
+ old_name='otp_enabled',
17
+ new_name='mfa_authenticator_enabled',
18
+ ),
19
+ migrations.RenameField(
20
+ model_name='user',
21
+ old_name='email_enabled',
22
+ new_name='mfa_email_enabled',
23
+ ),
24
+ migrations.RenameField(
25
+ model_name='user',
26
+ old_name='sms_enabled',
27
+ new_name='mfa_sms_enabled',
28
+ ),
29
+ ]
@@ -2,7 +2,7 @@ from .basic_api_errors import BasicAPIErrorTest
2
2
  from .basic_api_test import BasicAPITest
3
3
  from .test_api_doc import ApiDocTest
4
4
  from .test_api_doc_search import TestAPIDocSearch
5
- from .test_auth_api import AuthAPITest
5
+ from .test_auth_api import AuthAPITest, MFALoginTest
6
6
  from .test_body_mixin import TestBodyMixin
7
7
  from .test_cache import TestCache
8
8
  from .test_client import TestApiClient
@@ -924,6 +924,7 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
924
924
 
925
925
  def enable_otp(self, user):
926
926
  user.otp_secret_token = pyotp.random_base32()
927
+ user.mfa_authenticator_enabled = True
927
928
  user.save()
928
929
  user.refresh_from_db()
929
930
  return pyotp.totp.TOTP(user.otp_secret_token)
@@ -948,10 +949,10 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
948
949
  @override_settings(
949
950
  PFX_OTP_IMAGE="https://example.org/fake.png",
950
951
  PFX_OTP_COLOR="FF0000")
951
- def test_otp_enable(self):
952
+ def test_mfa_authenticator_enabled(self):
952
953
  self.client.login(username='jrr.tolkien', password='RIGHT PASSWORD')
953
954
 
954
- self.assertEqual(self.user1.is_otp, False)
955
+ self.assertEqual(self.user1.is_mfa, False)
955
956
  response = self.client.get('/api/auth/otp/setup-uri')
956
957
  self.assertRC(response, 200)
957
958
  self.assertJIn(response, 'setup_uri', "otpauth://totp/")
@@ -978,9 +979,9 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
978
979
  self.user1.refresh_from_db()
979
980
  self.assertEqual(len(self.user1.otp_secret_token), 32)
980
981
  self.assertIsNone(self.user1.otp_secret_token_tmp)
981
- self.assertEqual(self.user1.is_otp, True)
982
+ self.assertEqual(self.user1.is_mfa, True)
982
983
 
983
- def test_otp_enable_bad_code(self):
984
+ def test_mfa_authenticator_enabled_bad_code(self):
984
985
  self.client.login(username='jrr.tolkien', password='RIGHT PASSWORD')
985
986
 
986
987
  response = self.client.get('/api/auth/otp/setup-uri')
@@ -1009,7 +1010,7 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
1009
1010
  def test_otp_disable(self):
1010
1011
  totp = self.enable_otp(self.user1)
1011
1012
 
1012
- self.assertEqual(self.user1.is_otp, True)
1013
+ self.assertEqual(self.user1.is_mfa, True)
1013
1014
  self.otp_login(self.user1, 'RIGHT PASSWORD')
1014
1015
  response = self.client.put('/api/auth/otp/disable', dict(
1015
1016
  otp_code=totp.now()))
@@ -1018,7 +1019,7 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
1018
1019
  self.user1.refresh_from_db()
1019
1020
  self.assertIsNone(self.user1.otp_secret_token)
1020
1021
  self.assertIsNone(self.user1.otp_secret_token_tmp)
1021
- self.assertEqual(self.user1.is_otp, False)
1022
+ self.assertEqual(self.user1.is_mfa, False)
1022
1023
 
1023
1024
  @override_settings(
1024
1025
  PFX_HOTP_CODE_VALIDITY=15,
@@ -1090,8 +1091,8 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
1090
1091
  token=token,
1091
1092
  otp_code=totp.now()))
1092
1093
  self.assertRC(response, 200)
1093
- self.assertJE(response, 'user.is_otp', True)
1094
- self.assertJE(response, 'mfa_setup_required', False)
1094
+ self.assertJE(response, 'user.is_mfa', True)
1095
+ self.assertJE(response, 'user.mfa_setup_required', False)
1095
1096
  headers = jwt.get_unverified_header(token)
1096
1097
  self.assertEqual(headers['pfx_user_pk'], self.user1.pk)
1097
1098
  decoded = jwt.decode(
@@ -1103,6 +1104,7 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
1103
1104
  self.assertEqual(
1104
1105
  datetime.fromtimestamp(decoded['exp']),
1105
1106
  datetime(2023, 5, 1, 8, 40))
1107
+ response.print()
1106
1108
 
1107
1109
  @override_settings(PFX_TOKEN_LONG_VALIDITY={'days': 1})
1108
1110
  def test_otp_login_remember_me(self):
@@ -1444,7 +1446,7 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
1444
1446
  """Login with email+authenticator:
1445
1447
  email (highest priority) is active backend."""
1446
1448
  self.enable_otp(self.user1)
1447
- self.user1.email_enabled = True
1449
+ self.user1.mfa_email_enabled = True
1448
1450
  self.user1.save()
1449
1451
  with patch(
1450
1452
  'pfx.pfxcore.views.authentication_views'
@@ -1458,19 +1460,19 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
1458
1460
  self.assertJE(response, 'available_backends.@0.id', 'email')
1459
1461
  self.assertJE(response, 'available_backends.@1.id', 'authenticator')
1460
1462
 
1461
- # --- mfa_setup_required ---
1463
+ # --- user.mfa_setup_required ---
1462
1464
 
1463
1465
  def test_login_success_mfa_setup_required_false_by_default(self):
1464
1466
  """Direct login, no MFA force: mfa_setup_required=False."""
1465
1467
  response = self.do_login()
1466
1468
  self.assertRC(response, 200)
1467
1469
  self.assertJE(response, 'need_otp', False)
1468
- self.assertJE(response, 'mfa_setup_required', False)
1470
+ self.assertJE(response, 'user.mfa_setup_required', False)
1469
1471
 
1470
1472
  @override_settings(PFX_MFA_FORCE=True)
1471
1473
  def test_login_success_mfa_setup_required_true(self):
1472
1474
  """After OTP login, MFA forced, setup not dismissed:
1473
- mfa_setup_required=True."""
1475
+ user.mfa_setup_required=True."""
1474
1476
  totp = self.enable_otp(self.user1)
1475
1477
  response = self.do_login()
1476
1478
  self.assertRC(response, 200)
@@ -1478,12 +1480,12 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
1478
1480
  response = self.client.post('/api/auth/otp/login', dict(
1479
1481
  token=otp_token, otp_code=totp.now()))
1480
1482
  self.assertRC(response, 200)
1481
- self.assertJE(response, 'mfa_setup_required', True)
1483
+ self.assertJE(response, 'user.mfa_setup_required', True)
1482
1484
 
1483
1485
  @override_settings(PFX_MFA_FORCE=True)
1484
1486
  def test_login_success_mfa_setup_required_false_when_dismissed(self):
1485
1487
  """After OTP login, MFA forced, setup dismissed:
1486
- mfa_setup_required=False."""
1488
+ user.mfa_setup_required=False."""
1487
1489
  totp = self.enable_otp(self.user1)
1488
1490
  self.user1.mfa_setup_dismissed = True
1489
1491
  self.user1.save()
@@ -1493,14 +1495,14 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
1493
1495
  response = self.client.post('/api/auth/otp/login', dict(
1494
1496
  token=otp_token, otp_code=totp.now()))
1495
1497
  self.assertRC(response, 200)
1496
- self.assertJE(response, 'mfa_setup_required', False)
1498
+ self.assertJE(response, 'user.mfa_setup_required', False)
1497
1499
 
1498
1500
  # --- email backend ---
1499
1501
 
1500
1502
  @override_settings(PFX_MFA_BACKENDS=['email'])
1501
1503
  def test_login_email_backend_sends_challenge(self):
1502
1504
  """Login with email backend: sends email and returns is_oob=True."""
1503
- self.user1.email_enabled = True
1505
+ self.user1.mfa_email_enabled = True
1504
1506
  self.user1.otp_secret_token = pyotp.random_base32()
1505
1507
  self.user1.save()
1506
1508
  response = self.do_login()
@@ -1515,7 +1517,7 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
1515
1517
  def test_login_email_backend_generates_token_if_missing(self):
1516
1518
  """Login with email backend auto-generates
1517
1519
  otp_secret_token if absent."""
1518
- self.user1.email_enabled = True
1520
+ self.user1.mfa_email_enabled = True
1519
1521
  self.user1.save()
1520
1522
  self.assertIsNone(self.user1.otp_secret_token)
1521
1523
  response = self.do_login()
@@ -1531,7 +1533,7 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
1531
1533
  def test_login_sms_backend_calls_send_sms(self):
1532
1534
  """Login with SMS backend: calls send_mfa_sms_challenge,
1533
1535
  returns is_oob=True."""
1534
- self.user1.sms_enabled = True
1536
+ self.user1.mfa_sms_enabled = True
1535
1537
  self.user1.otp_secret_token = pyotp.random_base32()
1536
1538
  self.user1.save()
1537
1539
  with patch(
@@ -1550,7 +1552,7 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
1550
1552
  def test_login_forced_mfa_fallback_to_email(self):
1551
1553
  """MFA forced with no backend configured:
1552
1554
  falls back to email challenge."""
1553
- # No otp_secret_token, sms_enabled=False, email_enabled=False
1555
+ # No otp_secret_token, mfa_sms_enabled=False, mfa_email_enabled=False
1554
1556
  response = self.do_login()
1555
1557
  self.assertRC(response, 200)
1556
1558
  self.assertJE(response, 'need_otp', True)
@@ -1572,7 +1574,7 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
1572
1574
  @override_settings(PFX_MFA_BACKENDS=['authenticator', 'email'])
1573
1575
  def test_mfa_challenge_email_success(self):
1574
1576
  """POST /auth/mfa/challenge with email sends a new code."""
1575
- self.user1.email_enabled = True
1577
+ self.user1.mfa_email_enabled = True
1576
1578
  self.user1.otp_secret_token = pyotp.random_base32()
1577
1579
  self.user1.save()
1578
1580
  otp_token = self._get_otp_token()
@@ -1589,7 +1591,7 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
1589
1591
  """POST /auth/mfa/challenge: email allowed even without
1590
1592
  email_enabled when MFA forced."""
1591
1593
  otp_token = self._get_otp_token()
1592
- # user1 has email_enabled=False,
1594
+ # user1 has mfa_email_enabled=False,
1593
1595
  # but PFX_MFA_FORCE=True => forced_email is True
1594
1596
  response = self.client.post('/api/auth/mfa/challenge', {
1595
1597
  'token': otp_token,
@@ -1640,7 +1642,7 @@ class MFALoginTest(TestAssertMixin, TransactionTestCase):
1640
1642
  @override_settings(PFX_MFA_BACKENDS=['sms'])
1641
1643
  def test_mfa_challenge_sms_success(self):
1642
1644
  """POST /auth/mfa/challenge with sms calls send_mfa_sms_challenge."""
1643
- self.user1.sms_enabled = True
1645
+ self.user1.mfa_sms_enabled = True
1644
1646
  self.user1.otp_secret_token = pyotp.random_base32()
1645
1647
  self.user1.save()
1646
1648
  sms_path = (