django-pfx 1.4.dev104__tar.gz → 1.4.dev108__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 (192) hide show
  1. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/PKG-INFO +4 -2
  2. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/django_pfx.egg-info/PKG-INFO +4 -2
  3. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/django_pfx.egg-info/SOURCES.txt +5 -1
  4. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/django_pfx.egg-info/requires.txt +1 -0
  5. django_pfx-1.4.dev108/pfx/pfxcore/fields/__init__.py +3 -0
  6. django_pfx-1.4.dev108/pfx/pfxcore/fields/media_field.py +60 -0
  7. django_pfx-1.4.dev104/pfx/pfxcore/fields.py → django_pfx-1.4.dev108/pfx/pfxcore/fields/minutes_duration_field.py +0 -55
  8. django_pfx-1.4.dev108/pfx/pfxcore/fields/nh3_field.py +83 -0
  9. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.po +7 -7
  10. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/views/fields.py +11 -11
  11. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/views/rest_views.py +16 -21
  12. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/requirements.txt +1 -0
  13. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/setup.cfg +1 -0
  14. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/tests/__init__.py +1 -0
  15. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/tests/basic_api_test.py +12 -9
  16. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/tests/test_fields_minutes_duration.py +1 -1
  17. django_pfx-1.4.dev108/tests/tests/test_fields_nh3.py +38 -0
  18. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/.gitignore +0 -0
  19. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/.gitlab-ci.yml +0 -0
  20. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/.pre-commit-config.yaml +0 -0
  21. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/LICENSE +0 -0
  22. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/MANIFEST.in +0 -0
  23. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/README.md +0 -0
  24. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/django_pfx.egg-info/dependency_links.txt +0 -0
  25. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/django_pfx.egg-info/top_level.txt +0 -0
  26. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/doc/Makefile +0 -0
  27. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/doc/conf.py +0 -0
  28. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/doc/index.rst +0 -0
  29. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/doc/source/api.views.rst +0 -0
  30. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/doc/source/authentication.md +0 -0
  31. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/doc/source/decorator.md +0 -0
  32. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/doc/source/generate_openapi.md +0 -0
  33. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/doc/source/getting_started.md +0 -0
  34. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/doc/source/internationalisation.md +0 -0
  35. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/doc/source/model.md +0 -0
  36. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/doc/source/pfx_views.md +0 -0
  37. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/doc/source/profiling.md +0 -0
  38. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/doc/source/settings.md +0 -0
  39. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/doc/source/testing.md +0 -0
  40. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/img/pfx.png +0 -0
  41. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/img/pfx.svg +0 -0
  42. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/make_messages +0 -0
  43. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/manage.py +0 -0
  44. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/__init__.py +0 -0
  45. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/__init__.py +0 -0
  46. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/apidoc/__init__.py +0 -0
  47. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/apidoc/parameters.py +0 -0
  48. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/apidoc/schema.py +0 -0
  49. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/apidoc/tags.py +0 -0
  50. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/apps.py +0 -0
  51. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/decorator/__init__.py +0 -0
  52. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/decorator/rest.py +0 -0
  53. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/default_settings.py +0 -0
  54. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/exceptions.py +0 -0
  55. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/http/__init__.py +0 -0
  56. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/http/json_response.py +0 -0
  57. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
  58. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/management/__init__.py +0 -0
  59. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/management/commands/__init__.py +0 -0
  60. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/management/commands/makeapidoc.py +0 -0
  61. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/management/commands/profile.py +0 -0
  62. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/middleware/__init__.py +0 -0
  63. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/middleware/authentication.py +0 -0
  64. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/middleware/locale.py +0 -0
  65. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/middleware/profiling.py +0 -0
  66. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/migrations/0001_initial.py +0 -0
  67. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/migrations/0002_pfxpermissionsuser.py +0 -0
  68. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/migrations/0003_delete_pfxpermissionsuser.py +0 -0
  69. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/migrations/__init__.py +0 -0
  70. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/migrations/operations/__init__.py +0 -0
  71. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/migrations/operations/permissions.py +0 -0
  72. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/models/__init__.py +0 -0
  73. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/models/abstract_pfx_base_user.py +0 -0
  74. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/models/cache_mixins.py +0 -0
  75. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/models/login_ban.py +0 -0
  76. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/models/not_null_fields.py +0 -0
  77. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/models/ordered_model_mixin.py +0 -0
  78. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/models/otp_user_mixin.py +0 -0
  79. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/models/pfx_models.py +0 -0
  80. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/models/pfx_user.py +0 -0
  81. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/models/user_filtered_queryset_mixin.py +0 -0
  82. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/serializers/__init__.py +0 -0
  83. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/serializers/json.py +0 -0
  84. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/settings.py +0 -0
  85. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/shortcuts.py +0 -0
  86. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/storage/__init__.py +0 -0
  87. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/storage/exceptions.py +0 -0
  88. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/storage/local_storage.py +0 -0
  89. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/storage/s3_storage.py +0 -0
  90. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/templates/registration/otp_code_email.txt +0 -0
  91. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/templates/registration/otp_code_subject.txt +0 -0
  92. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/templates/registration/password_reset_email.txt +0 -0
  93. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/templates/registration/password_reset_subject.txt +0 -0
  94. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/templates/registration/welcome_email.txt +0 -0
  95. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/templates/registration/welcome_subject.txt +0 -0
  96. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/test.py +0 -0
  97. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/urls.py +0 -0
  98. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/views/__init__.py +0 -0
  99. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/views/authentication_views.py +0 -0
  100. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/views/filters_views.py +0 -0
  101. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/views/locale_views.py +0 -0
  102. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/views/media_rest_view_mixin.py +0 -0
  103. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/views/ordered_rest_view_mixin.py +0 -0
  104. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/views/parameters/__init__.py +0 -0
  105. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/views/parameters/date_format.py +0 -0
  106. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/views/parameters/groups.py +0 -0
  107. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/views/parameters/list_count.py +0 -0
  108. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/views/parameters/list_items.py +0 -0
  109. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/views/parameters/list_mode.py +0 -0
  110. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/views/parameters/list_order.py +0 -0
  111. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/views/parameters/list_search.py +0 -0
  112. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/views/parameters/media_redirect.py +0 -0
  113. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/views/parameters/meta_fields.py +0 -0
  114. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/views/parameters/meta_filters.py +0 -0
  115. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/views/parameters/meta_orders.py +0 -0
  116. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/views/parameters/subset.py +0 -0
  117. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/views/parameters/subset_limit.py +0 -0
  118. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/views/parameters/subset_offset.py +0 -0
  119. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/views/parameters/subset_page.py +0 -0
  120. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/views/parameters/subset_page_size.py +0 -0
  121. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/pfxcore/views/parameters/subset_page_subset.py +0 -0
  122. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/settings/__init__.py +0 -0
  123. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pfx/settings/dev.py +0 -0
  124. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/pyproject.toml +0 -0
  125. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/serve-doc +0 -0
  126. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/setup.py +0 -0
  127. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/__init__.py +0 -0
  128. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/apps.py +0 -0
  129. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/locale/fr/LC_MESSAGES/django.po +0 -0
  130. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/migrations/0001_initial.py +0 -0
  131. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/migrations/__init__.py +0 -0
  132. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/models.py +0 -0
  133. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/settings/__init__.py +0 -0
  134. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/settings/ci.py +0 -0
  135. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/settings/common.py +0 -0
  136. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/settings/dev.py +0 -0
  137. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/settings/dev_custom_example.py +0 -0
  138. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/settings/dev_default.py +0 -0
  139. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/tests/basic_api_errors.py +0 -0
  140. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/tests/test_api_doc.py +0 -0
  141. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/tests/test_api_doc_search.py +0 -0
  142. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/tests/test_auth_api.py +0 -0
  143. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/tests/test_body_mixin.py +0 -0
  144. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/tests/test_cache.py +0 -0
  145. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/tests/test_client.py +0 -0
  146. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/tests/test_fields_choices.py +0 -0
  147. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/tests/test_filters.py +0 -0
  148. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/tests/test_locale_api.py +0 -0
  149. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/tests/test_ordered_rest_view_mixin.py +0 -0
  150. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/tests/test_perm_tests.py +0 -0
  151. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/tests/test_permissions.py +0 -0
  152. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/tests/test_perms_api.py +0 -0
  153. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/tests/test_post_migrate_groups_update.py +0 -0
  154. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/tests/test_profiling_middleware.py +0 -0
  155. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/tests/test_settings.py +0 -0
  156. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/tests/test_shortcuts.py +0 -0
  157. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/tests/test_timezone_middleware.py +0 -0
  158. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/tests/test_tools.py +0 -0
  159. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/tests/test_user_queryset.py +0 -0
  160. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/tests/test_view_decorators.py +0 -0
  161. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/tests/test_view_fields.py +0 -0
  162. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/urls.py +0 -0
  163. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests/views.py +0 -0
  164. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests_base_user/__init__.py +0 -0
  165. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests_base_user/migrations/0001_initial.py +0 -0
  166. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests_base_user/migrations/__init__.py +0 -0
  167. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests_base_user/settings/__init__.py +0 -0
  168. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests_base_user/settings/ci.py +0 -0
  169. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests_base_user/settings/common.py +0 -0
  170. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests_base_user/settings/dev.py +0 -0
  171. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests_base_user/settings/dev_custom_example.py +0 -0
  172. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests_base_user/settings/dev_default.py +0 -0
  173. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests_base_user/tests/__init__.py +0 -0
  174. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests_base_user/tests/test_api.py +0 -0
  175. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests_base_user/tests/test_auth_api.py +0 -0
  176. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests_base_user/urls.py +0 -0
  177. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests_base_user/views.py +0 -0
  178. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests_custom_user/__init__.py +0 -0
  179. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests_custom_user/migrations/0001_initial.py +0 -0
  180. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests_custom_user/migrations/__init__.py +0 -0
  181. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests_custom_user/models.py +0 -0
  182. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests_custom_user/settings/__init__.py +0 -0
  183. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests_custom_user/settings/ci.py +0 -0
  184. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests_custom_user/settings/common.py +0 -0
  185. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests_custom_user/settings/dev.py +0 -0
  186. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests_custom_user/settings/dev_custom_example.py +0 -0
  187. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests_custom_user/settings/dev_default.py +0 -0
  188. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests_custom_user/tests/__init__.py +0 -0
  189. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests_custom_user/tests/test_api.py +0 -0
  190. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests_custom_user/tests/test_auth_api.py +0 -0
  191. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests_custom_user/urls.py +0 -0
  192. {django_pfx-1.4.dev104 → django_pfx-1.4.dev108}/tests_custom_user/views.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: django-pfx
3
- Version: 1.4.dev104
3
+ Version: 1.4.dev108
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
@@ -34,8 +34,10 @@ Requires-Dist: apispec
34
34
  Requires-Dist: pyyaml
35
35
  Requires-Dist: pytz
36
36
  Requires-Dist: dill
37
+ Requires-Dist: nh3
37
38
  Provides-Extra: otp
38
39
  Requires-Dist: pyotp; extra == "otp"
40
+ Dynamic: license-file
39
41
 
40
42
  # Django PFX
41
43
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: django-pfx
3
- Version: 1.4.dev104
3
+ Version: 1.4.dev108
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
@@ -34,8 +34,10 @@ Requires-Dist: apispec
34
34
  Requires-Dist: pyyaml
35
35
  Requires-Dist: pytz
36
36
  Requires-Dist: dill
37
+ Requires-Dist: nh3
37
38
  Provides-Extra: otp
38
39
  Requires-Dist: pyotp; extra == "otp"
40
+ Dynamic: license-file
39
41
 
40
42
  # Django PFX
41
43
 
@@ -37,7 +37,6 @@ pfx/pfxcore/__init__.py
37
37
  pfx/pfxcore/apps.py
38
38
  pfx/pfxcore/default_settings.py
39
39
  pfx/pfxcore/exceptions.py
40
- pfx/pfxcore/fields.py
41
40
  pfx/pfxcore/settings.py
42
41
  pfx/pfxcore/shortcuts.py
43
42
  pfx/pfxcore/test.py
@@ -48,6 +47,10 @@ pfx/pfxcore/apidoc/schema.py
48
47
  pfx/pfxcore/apidoc/tags.py
49
48
  pfx/pfxcore/decorator/__init__.py
50
49
  pfx/pfxcore/decorator/rest.py
50
+ pfx/pfxcore/fields/__init__.py
51
+ pfx/pfxcore/fields/media_field.py
52
+ pfx/pfxcore/fields/minutes_duration_field.py
53
+ pfx/pfxcore/fields/nh3_field.py
51
54
  pfx/pfxcore/http/__init__.py
52
55
  pfx/pfxcore/http/json_response.py
53
56
  pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo
@@ -141,6 +144,7 @@ tests/tests/test_cache.py
141
144
  tests/tests/test_client.py
142
145
  tests/tests/test_fields_choices.py
143
146
  tests/tests/test_fields_minutes_duration.py
147
+ tests/tests/test_fields_nh3.py
144
148
  tests/tests/test_filters.py
145
149
  tests/tests/test_locale_api.py
146
150
  tests/tests/test_ordered_rest_view_mixin.py
@@ -7,6 +7,7 @@ apispec
7
7
  pyyaml
8
8
  pytz
9
9
  dill
10
+ nh3
10
11
 
11
12
  [otp]
12
13
  pyotp
@@ -0,0 +1,3 @@
1
+ from .media_field import MediaField
2
+ from .minutes_duration_field import MinutesDurationField
3
+ from .nh3_field import NH3Field
@@ -0,0 +1,60 @@
1
+ import logging
2
+ from importlib import import_module
3
+
4
+ from django.db import models
5
+ from django.db.models.signals import post_delete
6
+ from django.dispatch import receiver
7
+
8
+ from pfx.pfxcore.shortcuts import settings
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def get_storage_class(class_path):
14
+ ps = class_path.split('.')
15
+ return getattr(import_module('.'.join(ps[:-1])), ps[-1])()
16
+
17
+
18
+ class MediaField(models.JSONField):
19
+ def __init__(
20
+ self, *args, max_length=255, get_key=None, storage=None,
21
+ auto_delete=False, **kwargs):
22
+ self.get_key = get_key or self.get_default_key
23
+ if not storage and not settings.STORAGE_DEFAULT:
24
+ raise Exception(
25
+ "Missing storage. You have to set a storage "
26
+ "class on the field or define STORAGE_DEFAULT settings.")
27
+ self.storage = storage or get_storage_class(settings.STORAGE_DEFAULT)
28
+ self.auto_delete = auto_delete
29
+ super().__init__(
30
+ *args, max_length=max_length,
31
+ default=kwargs.pop('default', dict),
32
+ blank=kwargs.pop('blank', True),
33
+ **kwargs)
34
+
35
+ @staticmethod
36
+ def get_default_key(obj, filename):
37
+ return f"{type(obj).__name__}/{obj.pk}/{filename}"
38
+
39
+ def to_python(self, value):
40
+ return super().to_python(self.storage.to_python(value))
41
+
42
+ def get_upload_url(self, request, obj, filename):
43
+ key = self.get_key(obj, filename)
44
+ url = self.storage.get_upload_url(request, key)
45
+ return dict(url=url, file=dict(name=filename, key=key))
46
+
47
+ def get_url(self, request, obj):
48
+ return self.storage.get_url(
49
+ request, self.value_from_object(obj)['key'])
50
+
51
+ def upload(self, obj, file, filename, **kwargs):
52
+ key = self.get_key(obj, filename)
53
+ return self.to_python(self.storage.upload(key, file, **kwargs))
54
+
55
+
56
+ @receiver(post_delete)
57
+ def post_delete_media(sender, instance, **kwargs):
58
+ for field in sender._meta.fields:
59
+ if isinstance(field, MediaField) and field.auto_delete:
60
+ field.storage.delete(field.value_from_object(instance))
@@ -1,69 +1,14 @@
1
1
  import logging
2
2
  import re
3
3
  from datetime import timedelta
4
- from importlib import import_module
5
4
 
6
5
  from django.core.exceptions import ValidationError
7
6
  from django.db import models
8
- from django.db.models.signals import post_delete
9
- from django.dispatch import receiver
10
7
  from django.utils.translation import gettext_lazy as _
11
8
 
12
- from pfx.pfxcore.shortcuts import settings
13
-
14
9
  logger = logging.getLogger(__name__)
15
10
 
16
11
 
17
- def get_storage_class(class_path):
18
- ps = class_path.split('.')
19
- return getattr(import_module('.'.join(ps[:-1])), ps[-1])()
20
-
21
-
22
- class MediaField(models.JSONField):
23
- def __init__(
24
- self, *args, max_length=255, get_key=None, storage=None,
25
- auto_delete=False, **kwargs):
26
- self.get_key = get_key or self.get_default_key
27
- if not storage and not settings.STORAGE_DEFAULT:
28
- raise Exception(
29
- "Missing storage. You have to set a storage "
30
- "class on the field or define STORAGE_DEFAULT settings.")
31
- self.storage = storage or get_storage_class(settings.STORAGE_DEFAULT)
32
- self.auto_delete = auto_delete
33
- super().__init__(
34
- *args, max_length=max_length,
35
- default=kwargs.pop('default', dict),
36
- blank=kwargs.pop('blank', True),
37
- **kwargs)
38
-
39
- @staticmethod
40
- def get_default_key(obj, filename):
41
- return f"{type(obj).__name__}/{obj.pk}/{filename}"
42
-
43
- def to_python(self, value):
44
- return super().to_python(self.storage.to_python(value))
45
-
46
- def get_upload_url(self, request, obj, filename):
47
- key = self.get_key(obj, filename)
48
- url = self.storage.get_upload_url(request, key)
49
- return dict(url=url, file=dict(name=filename, key=key))
50
-
51
- def get_url(self, request, obj):
52
- return self.storage.get_url(
53
- request, self.value_from_object(obj)['key'])
54
-
55
- def upload(self, obj, file, filename, **kwargs):
56
- key = self.get_key(obj, filename)
57
- return self.to_python(self.storage.upload(key, file, **kwargs))
58
-
59
-
60
- @receiver(post_delete)
61
- def post_delete_media(sender, instance, **kwargs):
62
- for field in sender._meta.fields:
63
- if isinstance(field, MediaField) and field.auto_delete:
64
- field.storage.delete(field.value_from_object(instance))
65
-
66
-
67
12
  class MinutesDurationField(models.DurationField):
68
13
  RE_FLOAT = re.compile(r'^[0-9]*(\.[0-9]*)?$')
69
14
  RE_HH_MM = re.compile(r'^([0-9]*):([0-5][0-9])?$')
@@ -0,0 +1,83 @@
1
+ from django.conf import settings
2
+ from django.db import models
3
+ from django.utils.safestring import mark_safe
4
+
5
+ import nh3
6
+
7
+
8
+ def get_nh3_default_options():
9
+ nh3_args = {}
10
+ nh3_settings = {
11
+ "NH3_ALLOWED_TAGS": "tags",
12
+ "NH3_ALLOWED_ATTRIBUTES": "attributes",
13
+ "NH3_STRIP_COMMENTS": "strip_comments",
14
+ "NH3_URL_SCHEMES": "url_schemes",
15
+ "NH3_ATTRIBUTE_FILTER": "attribute_filter",
16
+ "NH3_LINK_REL": "link_rel",
17
+ "NH3_GENERIC_ATTRIBUTE_PREFIXES": "generic_attribute_prefixes",
18
+ "NH3_TAG_ATTRIBUTE_VALUES": "tag_attribute_values",
19
+ "NH3_SET_TAG_ATTRIBUTE_VALUES": "set_tag_attribute_values",
20
+ }
21
+
22
+ for setting, kwarg in nh3_settings.items():
23
+ if hasattr(settings, setting):
24
+ attr = getattr(settings, setting)
25
+ nh3_args[kwarg] = attr
26
+
27
+ return nh3_args
28
+
29
+
30
+ class NH3Field(models.TextField):
31
+ def __init__(
32
+ self,
33
+ allowed_tags=None,
34
+ allowed_attributes=None,
35
+ url_schemes=None,
36
+ strip_comments=None,
37
+ attribute_filter=None,
38
+ link_rel=None,
39
+ generic_attribute_prefixes=None,
40
+ tag_attribute_values=None,
41
+ set_tag_attribute_values=None,
42
+ *args,
43
+ **kwargs,
44
+ ):
45
+ super().__init__(*args, **kwargs)
46
+
47
+ self.nh3_kwargs = get_nh3_default_options()
48
+
49
+ if allowed_tags:
50
+ self.nh3_kwargs["tags"] = allowed_tags
51
+ if allowed_attributes:
52
+ self.nh3_kwargs["attributes"] = allowed_attributes
53
+ if url_schemes:
54
+ self.nh3_kwargs["url_schemes"] = url_schemes
55
+ if strip_comments:
56
+ self.nh3_kwargs["strip_comments"] = strip_comments
57
+ if attribute_filter:
58
+ self.nh3_kwargs["attribute_filter"] = attribute_filter
59
+ if link_rel:
60
+ self.nh3_kwargs["link_rel"] = link_rel
61
+ if generic_attribute_prefixes:
62
+ self.nh3_kwargs["generic_attribute_prefixes"] = (
63
+ generic_attribute_prefixes)
64
+ if tag_attribute_values:
65
+ self.nh3_kwargs["tag_attribute_values"] = tag_attribute_values
66
+ if set_tag_attribute_values:
67
+ self.nh3_kwargs["set_tag_attribute_values"] = (
68
+ set_tag_attribute_values)
69
+
70
+ def pre_save(self, model_instance, add):
71
+ data = getattr(model_instance, self.attname)
72
+ if data is None:
73
+ return data
74
+ clean_value = nh3.clean(data, **self.nh3_kwargs) if data else ""
75
+ setattr(model_instance, self.attname, mark_safe(clean_value))
76
+ return clean_value
77
+
78
+ def from_db_value(self, value, expression, connection):
79
+ if value is None:
80
+ return value
81
+ # Values are sanitised before saving, so any value returned from the DB
82
+ # is safe to render unescaped.
83
+ return mark_safe(value)
@@ -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: 2025-03-05 17:31+0100\n"
10
+ "POT-Creation-Date: 2025-03-31 12:17+0200\n"
11
11
  "PO-Revision-Date: 2021-06-22 23:31+0200\n"
12
12
  "Last-Translator: \n"
13
13
  "Language-Team: \n"
@@ -47,11 +47,11 @@ msgstr "Interdit"
47
47
  msgid "Resource not found"
48
48
  msgstr "Ressource non trouvée"
49
49
 
50
- #: fields.py:86
50
+ #: fields/minutes_duration_field.py:31
51
51
  msgid "Invalid value."
52
52
  msgstr "Valeur invalide."
53
53
 
54
- #: fields.py:101
54
+ #: fields/minutes_duration_field.py:46
55
55
  msgid ""
56
56
  "Invalid format, it can be a number in hours, “1:05”, “:05”, “1h 5m”, “1.5h” "
57
57
  "or “30m”."
@@ -281,23 +281,23 @@ msgstr "Le paramètre object est requis pour move={move}."
281
281
  msgid "object {pk} does not exists in this move context."
282
282
  msgstr "object {pk} n'existe pas dans ce contexte de déplacement."
283
283
 
284
- #: views/rest_views.py:239
284
+ #: views/rest_views.py:234
285
285
  #, python-brace-format
286
286
  msgid "{obj} cannot be deleted because it is referenced by other objects."
287
287
  msgstr ""
288
288
  "{obj} ne peut pas être supprimé car il est référencé par d’autres objets."
289
289
 
290
- #: views/rest_views.py:324
290
+ #: views/rest_views.py:319
291
291
  #, python-brace-format
292
292
  msgid "{model} {obj} created."
293
293
  msgstr "{model} {obj} créé."
294
294
 
295
- #: views/rest_views.py:325
295
+ #: views/rest_views.py:320
296
296
  #, python-brace-format
297
297
  msgid "{model} {obj} updated."
298
298
  msgstr "{model} {obj} modifié."
299
299
 
300
- #: views/rest_views.py:1170
300
+ #: views/rest_views.py:1165
301
301
  #, python-brace-format
302
302
  msgid "{model} {obj} deleted."
303
303
  msgstr "{model} {obj} supprimé."
@@ -1,11 +1,10 @@
1
1
  import inspect
2
2
  import logging
3
3
  import operator
4
- from datetime import date
5
4
 
5
+ from django.core.exceptions import ValidationError
6
6
  from django.db import models
7
7
  from django.db.models.constants import LOOKUP_SEP
8
- from django.utils.formats import date_format
9
8
  from django.utils.functional import cached_property
10
9
 
11
10
  from apispec.utils import deepupdate
@@ -144,13 +143,8 @@ class ViewField:
144
143
  return self.json_repr(value)
145
144
  return value.json_repr()
146
145
 
147
- def to_json(self, obj, format_date):
146
+ def to_json(self, obj):
148
147
  value = self.get_value(obj)
149
- if format_date and isinstance(value, date):
150
- return dict(
151
- value=value,
152
- formatted=date_format(
153
- value, format='SHORT_DATE_FORMAT', use_l10n=True))
154
148
  if self.field_type == FieldType.MinutesDurationField:
155
149
  return pfx_fields.MinutesDurationField.to_json(value)
156
150
  if self.field_type == FieldType.ModelObject:
@@ -343,9 +337,15 @@ class ViewModelField(ViewField):
343
337
  if isinstance(value, dict) and 'value' in value
344
338
  else value)
345
339
  elif self.field_type == FieldType.DateField:
346
- value = (value['value']
347
- if isinstance(value, dict) and 'value' in value
348
- else value)
340
+ try:
341
+ value = value and field.to_python(value)
342
+ except ValidationError as e:
343
+ raise ValidationError({field.name: e})
344
+ elif self.field_type == FieldType.DateTimeField:
345
+ try:
346
+ value = value and field.to_python(value)
347
+ except ValidationError as e:
348
+ raise ValidationError({field.name: e})
349
349
  elif self.field_type == FieldType.IntegerField:
350
350
  value = value if value != '' else None
351
351
  elif self.field_type == FieldType.FloatField:
@@ -205,11 +205,6 @@ class ModelMixin():
205
205
  """
206
206
  return self.model._meta.verbose_name
207
207
 
208
- @property
209
- def format_date(self):
210
- """:meta private: Undocumented because it must be changed."""
211
- return get_bool(self.request.GET, 'date_format')
212
-
213
208
  def message_response(self, message, **kwargs):
214
209
  """Build a message JSON response.
215
210
 
@@ -296,7 +291,7 @@ class ModelResponseMixin(ModelMixin):
296
291
  :rtype: :class:`JsonResponse`
297
292
  """
298
293
  return JsonResponse(self.serialize_object(o, **{
299
- _f.alias: _f.to_json(o, self.format_date)
294
+ _f.alias: _f.to_json(o)
300
295
  for _f in self.get_fields().values()}, meta=meta))
301
296
 
302
297
  def validate(self, obj, rel_data=None, created=False, **kwargs):
@@ -759,7 +754,7 @@ class ListRestViewMixin(ModelResponseMixin):
759
754
  *self.get_list_fields_prefetch_related())
760
755
  for o in qs:
761
756
  yield self.serialize_object(o, **{
762
- _f.alias: _f.to_json(o, self.format_date)
757
+ _f.alias: _f.to_json(o)
763
758
  for _f in self.get_list_fields().values()})
764
759
 
765
760
  def get_short_list_result(self, qs):
@@ -1043,14 +1038,14 @@ class CreateRestViewMixin(ModelBodyMixin, ModelResponseMixin):
1043
1038
  return True
1044
1039
 
1045
1040
  def _post(self, *args, **kwargs):
1046
- obj = self.new_object()
1047
- data, rel_data = self.get_model_data(
1048
- obj, self.deserialize_body(), created=True)
1049
- forbidden = False
1050
- if not self.object_create_perm(data):
1051
- forbidden = True
1052
- self.set_values(obj, **data)
1053
1041
  try:
1042
+ obj = self.new_object()
1043
+ data, rel_data = self.get_model_data(
1044
+ obj, self.deserialize_body(), created=True)
1045
+ forbidden = False
1046
+ if not self.object_create_perm(data):
1047
+ forbidden = True
1048
+ self.set_values(obj, **data)
1054
1049
  self.validate(obj, rel_data=rel_data, created=True)
1055
1050
  if forbidden:
1056
1051
  raise ForbiddenError
@@ -1101,14 +1096,14 @@ class UpdateRestViewMixin(ModelBodyMixin, ModelResponseMixin):
1101
1096
  return True
1102
1097
 
1103
1098
  def _put(self, id, *args, **kwargs):
1104
- obj = self.get_object(pk=id)
1105
- data, rel_data = self.get_model_data(
1106
- obj, self.deserialize_body(), created=False)
1107
- forbidden = False
1108
- if not self.object_update_perm(obj, data):
1109
- forbidden = True
1110
- self.set_values(obj, **data)
1111
1099
  try:
1100
+ obj = self.get_object(pk=id)
1101
+ data, rel_data = self.get_model_data(
1102
+ obj, self.deserialize_body(), created=False)
1103
+ forbidden = False
1104
+ if not self.object_update_perm(obj, data):
1105
+ forbidden = True
1106
+ self.set_values(obj, **data)
1112
1107
  self.validate(obj, rel_data=rel_data, created=False)
1113
1108
  if forbidden:
1114
1109
  raise ForbiddenError
@@ -20,3 +20,4 @@ apispec
20
20
  pyyaml
21
21
  pytz
22
22
  pyotp
23
+ nh3
@@ -40,6 +40,7 @@ install_requires =
40
40
  pyyaml
41
41
  pytz
42
42
  dill
43
+ nh3
43
44
 
44
45
  [options.extras_require]
45
46
  otp = pyotp
@@ -8,6 +8,7 @@ from .test_cache import TestCache
8
8
  from .test_client import TestApiClient
9
9
  from .test_fields_choices import TestFieldsChoices
10
10
  from .test_fields_minutes_duration import TestFieldsMinutesDuration
11
+ from .test_fields_nh3 import TestFieldsNh3
11
12
  from .test_filters import FiltersTest
12
13
  from .test_locale_api import LocaleAPITest
13
14
  from .test_ordered_rest_view_mixin import TestOrderedRestViewMixin
@@ -632,15 +632,6 @@ class BasicAPITest(TestAssertMixin, TestCase):
632
632
  response = self.client.get(f'/api/books/{self.author1_book1.pk}')
633
633
  self.assertRC(response, 200)
634
634
  self.assertJE(response, 'pub_date', '1937-01-01')
635
- response = self.client.get(
636
- f'/api/books/{self.author1_book1.pk}?date_format=0')
637
- self.assertRC(response, 200)
638
- self.assertJE(response, 'pub_date', '1937-01-01')
639
- response = self.client.get(
640
- f'/api/books/{self.author1_book1.pk}?date_format=1')
641
- self.assertRC(response, 200)
642
- self.assertJE(response, 'pub_date.value', '1937-01-01')
643
- self.assertJE(response, 'pub_date.formatted', '01/01/1937')
644
635
 
645
636
  def test_get_detail_with_rel_object(self):
646
637
  response = self.client.get(f'/api/books/{self.author3_book1.pk}')
@@ -764,6 +755,18 @@ class BasicAPITest(TestAssertMixin, TestCase):
764
755
  response, '__all__.@0',
765
756
  'The Hobbit already exists for John Ronald Reuel Tolkien')
766
757
 
758
+ def test_create_invalid_date(self):
759
+ response = self.client.post(
760
+ '/api/books', dict(
761
+ name="The Silmarillion",
762
+ author=self.author1.pk,
763
+ pub_date="19777-09-15"))
764
+ self.assertRC(response, 422)
765
+ self.assertJE(
766
+ response, 'pub_date.@0',
767
+ '“19777-09-15” value has an invalid date format. '
768
+ 'It must be in YYYY-MM-DD format.')
769
+
767
770
  def test_update(self):
768
771
  response = self.client.put(
769
772
  f'/api/authors/{self.author1.pk}',
@@ -29,7 +29,7 @@ class TestFieldsMinutesDuration(TestAssertMixin, TestCase):
29
29
  pub_date=date(1937, 1, 1)
30
30
  )
31
31
 
32
- @patch('pfx.pfxcore.fields.logger', MagicMock())
32
+ @patch('pfx.pfxcore.fields.minutes_duration_field.logger', MagicMock())
33
33
  def test_to_python(self):
34
34
  d = MinutesDurationField()
35
35
  self.assertIsNone(d.to_python(None))
@@ -0,0 +1,38 @@
1
+ from django.db import connection, models
2
+ from django.test import TestCase
3
+ from django.test.utils import override_settings
4
+
5
+ from pfx.pfxcore.fields import NH3Field
6
+ from pfx.pfxcore.models import JSONReprMixin
7
+ from pfx.pfxcore.test import TestAssertMixin
8
+
9
+
10
+ class TestNh3Model(JSONReprMixin, models.Model):
11
+ html = NH3Field()
12
+ custom_html = NH3Field(
13
+ allowed_tags={'h1', 'p', 'span'},
14
+ allowed_attributes={'*': {'class'}})
15
+
16
+ class Meta:
17
+ verbose_name = "TestModel"
18
+ verbose_name_plural = "TestModels"
19
+ ordering = ['pk']
20
+
21
+
22
+ @override_settings(ROOT_URLCONF=__name__)
23
+ class TestFieldsNh3(TestAssertMixin, TestCase):
24
+
25
+ @classmethod
26
+ def setUpTestData(cls):
27
+ with connection.schema_editor() as schema_editor:
28
+ schema_editor.create_model(TestNh3Model)
29
+
30
+ def test_save(self):
31
+ t = TestNh3Model.objects.create(
32
+ html="<unknown>AAA</unknown>",
33
+ custom_html='<h1>TEST</h1>'
34
+ '<script>alert("hello")</script>'
35
+ '<p><span class="test" style="font-family: noto;"></span></p>')
36
+ self.assertEqual(t.html, "AAA")
37
+ self.assertEqual(
38
+ t.custom_html, '<h1>TEST</h1><p><span class="test"></span></p>')
File without changes