django-pfx 1.4.dev102__tar.gz → 1.4.dev106__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 (193) hide show
  1. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/PKG-INFO +2 -1
  2. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/django_pfx.egg-info/PKG-INFO +2 -1
  3. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/django_pfx.egg-info/SOURCES.txt +8 -1
  4. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/django_pfx.egg-info/requires.txt +1 -0
  5. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/default_settings.py +5 -0
  6. django_pfx-1.4.dev106/pfx/pfxcore/fields/__init__.py +3 -0
  7. django_pfx-1.4.dev106/pfx/pfxcore/fields/media_field.py +60 -0
  8. django_pfx-1.4.dev102/pfx/pfxcore/fields.py → django_pfx-1.4.dev106/pfx/pfxcore/fields/minutes_duration_field.py +0 -46
  9. django_pfx-1.4.dev106/pfx/pfxcore/fields/nh3_field.py +83 -0
  10. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.po +11 -11
  11. django_pfx-1.4.dev106/pfx/pfxcore/storage/__init__.py +3 -0
  12. django_pfx-1.4.dev106/pfx/pfxcore/storage/exceptions.py +3 -0
  13. django_pfx-1.4.dev106/pfx/pfxcore/storage/local_storage.py +56 -0
  14. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/storage/s3_storage.py +7 -6
  15. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/__init__.py +1 -2
  16. django_pfx-1.4.dev106/pfx/pfxcore/views/media_rest_view_mixin.py +171 -0
  17. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/rest_views.py +1 -135
  18. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/requirements.txt +1 -0
  19. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/setup.cfg +1 -0
  20. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/settings/common.py +2 -0
  21. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/__init__.py +1 -0
  22. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_fields_minutes_duration.py +1 -1
  23. django_pfx-1.4.dev106/tests/tests/test_fields_nh3.py +38 -0
  24. django_pfx-1.4.dev102/pfx/pfxcore/storage/__init__.py +0 -3
  25. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/.gitignore +0 -0
  26. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/.gitlab-ci.yml +0 -0
  27. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/.pre-commit-config.yaml +0 -0
  28. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/LICENSE +0 -0
  29. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/MANIFEST.in +0 -0
  30. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/README.md +0 -0
  31. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/django_pfx.egg-info/dependency_links.txt +0 -0
  32. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/django_pfx.egg-info/top_level.txt +0 -0
  33. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/doc/Makefile +0 -0
  34. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/doc/conf.py +0 -0
  35. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/doc/index.rst +0 -0
  36. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/doc/source/api.views.rst +0 -0
  37. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/doc/source/authentication.md +0 -0
  38. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/doc/source/decorator.md +0 -0
  39. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/doc/source/generate_openapi.md +0 -0
  40. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/doc/source/getting_started.md +0 -0
  41. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/doc/source/internationalisation.md +0 -0
  42. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/doc/source/model.md +0 -0
  43. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/doc/source/pfx_views.md +0 -0
  44. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/doc/source/profiling.md +0 -0
  45. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/doc/source/settings.md +0 -0
  46. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/doc/source/testing.md +0 -0
  47. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/img/pfx.png +0 -0
  48. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/img/pfx.svg +0 -0
  49. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/make_messages +0 -0
  50. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/manage.py +0 -0
  51. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/__init__.py +0 -0
  52. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/__init__.py +0 -0
  53. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/apidoc/__init__.py +0 -0
  54. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/apidoc/parameters.py +0 -0
  55. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/apidoc/schema.py +0 -0
  56. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/apidoc/tags.py +0 -0
  57. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/apps.py +0 -0
  58. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/decorator/__init__.py +0 -0
  59. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/decorator/rest.py +0 -0
  60. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/exceptions.py +0 -0
  61. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/http/__init__.py +0 -0
  62. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/http/json_response.py +0 -0
  63. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
  64. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/management/__init__.py +0 -0
  65. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/management/commands/__init__.py +0 -0
  66. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/management/commands/makeapidoc.py +0 -0
  67. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/management/commands/profile.py +0 -0
  68. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/middleware/__init__.py +0 -0
  69. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/middleware/authentication.py +0 -0
  70. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/middleware/locale.py +0 -0
  71. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/middleware/profiling.py +0 -0
  72. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/migrations/0001_initial.py +0 -0
  73. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/migrations/0002_pfxpermissionsuser.py +0 -0
  74. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/migrations/0003_delete_pfxpermissionsuser.py +0 -0
  75. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/migrations/__init__.py +0 -0
  76. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/migrations/operations/__init__.py +0 -0
  77. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/migrations/operations/permissions.py +0 -0
  78. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/models/__init__.py +0 -0
  79. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/models/abstract_pfx_base_user.py +0 -0
  80. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/models/cache_mixins.py +0 -0
  81. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/models/login_ban.py +0 -0
  82. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/models/not_null_fields.py +0 -0
  83. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/models/ordered_model_mixin.py +0 -0
  84. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/models/otp_user_mixin.py +0 -0
  85. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/models/pfx_models.py +0 -0
  86. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/models/pfx_user.py +0 -0
  87. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/models/user_filtered_queryset_mixin.py +0 -0
  88. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/serializers/__init__.py +0 -0
  89. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/serializers/json.py +0 -0
  90. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/settings.py +0 -0
  91. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/shortcuts.py +0 -0
  92. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/templates/registration/otp_code_email.txt +0 -0
  93. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/templates/registration/otp_code_subject.txt +0 -0
  94. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/templates/registration/password_reset_email.txt +0 -0
  95. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/templates/registration/password_reset_subject.txt +0 -0
  96. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/templates/registration/welcome_email.txt +0 -0
  97. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/templates/registration/welcome_subject.txt +0 -0
  98. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/test.py +0 -0
  99. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/urls.py +0 -0
  100. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/authentication_views.py +0 -0
  101. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/fields.py +0 -0
  102. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/filters_views.py +0 -0
  103. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/locale_views.py +0 -0
  104. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/ordered_rest_view_mixin.py +0 -0
  105. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/__init__.py +0 -0
  106. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/date_format.py +0 -0
  107. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/groups.py +0 -0
  108. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/list_count.py +0 -0
  109. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/list_items.py +0 -0
  110. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/list_mode.py +0 -0
  111. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/list_order.py +0 -0
  112. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/list_search.py +0 -0
  113. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/media_redirect.py +0 -0
  114. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/meta_fields.py +0 -0
  115. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/meta_filters.py +0 -0
  116. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/meta_orders.py +0 -0
  117. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/subset.py +0 -0
  118. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/subset_limit.py +0 -0
  119. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/subset_offset.py +0 -0
  120. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/subset_page.py +0 -0
  121. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/subset_page_size.py +0 -0
  122. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/subset_page_subset.py +0 -0
  123. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/settings/__init__.py +0 -0
  124. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/settings/dev.py +0 -0
  125. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pyproject.toml +0 -0
  126. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/serve-doc +0 -0
  127. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/setup.py +0 -0
  128. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/__init__.py +0 -0
  129. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/apps.py +0 -0
  130. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/locale/fr/LC_MESSAGES/django.po +0 -0
  131. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/migrations/0001_initial.py +0 -0
  132. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/migrations/__init__.py +0 -0
  133. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/models.py +0 -0
  134. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/settings/__init__.py +0 -0
  135. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/settings/ci.py +0 -0
  136. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/settings/dev.py +0 -0
  137. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/settings/dev_custom_example.py +0 -0
  138. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/settings/dev_default.py +0 -0
  139. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/basic_api_errors.py +0 -0
  140. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/basic_api_test.py +0 -0
  141. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_api_doc.py +0 -0
  142. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_api_doc_search.py +0 -0
  143. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_auth_api.py +0 -0
  144. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_body_mixin.py +0 -0
  145. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_cache.py +0 -0
  146. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_client.py +0 -0
  147. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_fields_choices.py +0 -0
  148. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_filters.py +0 -0
  149. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_locale_api.py +0 -0
  150. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_ordered_rest_view_mixin.py +0 -0
  151. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_perm_tests.py +0 -0
  152. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_permissions.py +0 -0
  153. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_perms_api.py +0 -0
  154. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_post_migrate_groups_update.py +0 -0
  155. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_profiling_middleware.py +0 -0
  156. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_settings.py +0 -0
  157. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_shortcuts.py +0 -0
  158. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_timezone_middleware.py +0 -0
  159. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_tools.py +0 -0
  160. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_user_queryset.py +0 -0
  161. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_view_decorators.py +0 -0
  162. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_view_fields.py +0 -0
  163. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/urls.py +0 -0
  164. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/views.py +0 -0
  165. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_base_user/__init__.py +0 -0
  166. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_base_user/migrations/0001_initial.py +0 -0
  167. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_base_user/migrations/__init__.py +0 -0
  168. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_base_user/settings/__init__.py +0 -0
  169. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_base_user/settings/ci.py +0 -0
  170. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_base_user/settings/common.py +0 -0
  171. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_base_user/settings/dev.py +0 -0
  172. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_base_user/settings/dev_custom_example.py +0 -0
  173. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_base_user/settings/dev_default.py +0 -0
  174. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_base_user/tests/__init__.py +0 -0
  175. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_base_user/tests/test_api.py +0 -0
  176. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_base_user/tests/test_auth_api.py +0 -0
  177. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_base_user/urls.py +0 -0
  178. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_base_user/views.py +0 -0
  179. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_custom_user/__init__.py +0 -0
  180. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_custom_user/migrations/0001_initial.py +0 -0
  181. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_custom_user/migrations/__init__.py +0 -0
  182. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_custom_user/models.py +0 -0
  183. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_custom_user/settings/__init__.py +0 -0
  184. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_custom_user/settings/ci.py +0 -0
  185. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_custom_user/settings/common.py +0 -0
  186. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_custom_user/settings/dev.py +0 -0
  187. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_custom_user/settings/dev_custom_example.py +0 -0
  188. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_custom_user/settings/dev_default.py +0 -0
  189. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_custom_user/tests/__init__.py +0 -0
  190. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_custom_user/tests/test_api.py +0 -0
  191. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_custom_user/tests/test_auth_api.py +0 -0
  192. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_custom_user/urls.py +0 -0
  193. {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_custom_user/views.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: django-pfx
3
- Version: 1.4.dev102
3
+ Version: 1.4.dev106
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,6 +34,7 @@ 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"
39
40
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: django-pfx
3
- Version: 1.4.dev102
3
+ Version: 1.4.dev106
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,6 +34,7 @@ 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"
39
40
 
@@ -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
@@ -79,6 +82,8 @@ pfx/pfxcore/models/user_filtered_queryset_mixin.py
79
82
  pfx/pfxcore/serializers/__init__.py
80
83
  pfx/pfxcore/serializers/json.py
81
84
  pfx/pfxcore/storage/__init__.py
85
+ pfx/pfxcore/storage/exceptions.py
86
+ pfx/pfxcore/storage/local_storage.py
82
87
  pfx/pfxcore/storage/s3_storage.py
83
88
  pfx/pfxcore/templates/registration/otp_code_email.txt
84
89
  pfx/pfxcore/templates/registration/otp_code_subject.txt
@@ -91,6 +96,7 @@ pfx/pfxcore/views/authentication_views.py
91
96
  pfx/pfxcore/views/fields.py
92
97
  pfx/pfxcore/views/filters_views.py
93
98
  pfx/pfxcore/views/locale_views.py
99
+ pfx/pfxcore/views/media_rest_view_mixin.py
94
100
  pfx/pfxcore/views/ordered_rest_view_mixin.py
95
101
  pfx/pfxcore/views/rest_views.py
96
102
  pfx/pfxcore/views/parameters/__init__.py
@@ -138,6 +144,7 @@ tests/tests/test_cache.py
138
144
  tests/tests/test_client.py
139
145
  tests/tests/test_fields_choices.py
140
146
  tests/tests/test_fields_minutes_duration.py
147
+ tests/tests/test_fields_nh3.py
141
148
  tests/tests/test_filters.py
142
149
  tests/tests/test_locale_api.py
143
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
@@ -21,6 +21,8 @@ PFX_OPENAPI_TEMPLATE = {}
21
21
  PFX_PROFILING_SQL = ""
22
22
  PFX_PROFILING_CPROFILE = ""
23
23
 
24
+ STORAGE_DEFAULT = None
25
+
24
26
  STORAGE_S3_AWS_REGION = None
25
27
  STORAGE_S3_AWS_ACCESS_KEY = None
26
28
  STORAGE_S3_AWS_SECRET_KEY = None
@@ -29,6 +31,9 @@ STORAGE_S3_AWS_S3_BUCKET = None
29
31
  STORAGE_S3_AWS_PUT_URL_EXPIRE = None
30
32
  STORAGE_S3_AWS_GET_URL_EXPIRE = None
31
33
 
34
+ STORAGE_LOCAL_ROOT = None
35
+ STORAGE_LOCAL_X_ACCEL_REDIRECT = False
36
+
32
37
  PFX_TEST_MODE = False
33
38
 
34
39
  PFX_AUTH_GROUPS_CREATE_ONLY = False
@@ -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))
@@ -4,57 +4,11 @@ from datetime import timedelta
4
4
 
5
5
  from django.core.exceptions import ValidationError
6
6
  from django.db import models
7
- from django.db.models.signals import post_delete
8
- from django.dispatch import receiver
9
7
  from django.utils.translation import gettext_lazy as _
10
8
 
11
- from pfx.pfxcore.storage import DefaultStorage
12
-
13
9
  logger = logging.getLogger(__name__)
14
10
 
15
11
 
16
- class MediaField(models.JSONField):
17
- def __init__(
18
- self, *args, max_length=255, get_key=None, storage=None,
19
- auto_delete=False, **kwargs):
20
- self.get_key = get_key or self.get_default_key
21
- self.storage = storage or DefaultStorage()
22
- self.auto_delete = auto_delete
23
- super().__init__(
24
- *args, max_length=max_length,
25
- default=kwargs.pop('default', dict),
26
- blank=kwargs.pop('blank', True),
27
- **kwargs)
28
-
29
- @staticmethod
30
- def get_default_key(obj, filename):
31
- return f"{type(obj).__name__}/{obj.pk}/{filename}"
32
-
33
- def to_python(self, value):
34
- return super().to_python(self.storage.to_python(value))
35
-
36
- def get_upload_url(self, request, obj, filename):
37
- key = self.get_key(obj, filename)
38
- url = self.storage.get_upload_url(request, key)
39
- return dict(url=url, file=dict(name=filename, key=key))
40
-
41
- def get_url(self, request, obj):
42
- return self.storage.get_url(
43
- request, self.value_from_object(obj)['key'])
44
-
45
- def upload(self, obj, file, filename, **kwargs):
46
- key = self.get_key(obj, filename)
47
- self.storage.upload(key, file, **kwargs)
48
- return self.to_python(dict(key=key))
49
-
50
-
51
- @receiver(post_delete)
52
- def post_delete_media(sender, instance, **kwargs):
53
- for field in sender._meta.fields:
54
- if isinstance(field, MediaField) and field.auto_delete:
55
- field.storage.delete(field.value_from_object(instance))
56
-
57
-
58
12
  class MinutesDurationField(models.DurationField):
59
13
  RE_FLOAT = re.compile(r'^[0-9]*(\.[0-9]*)?$')
60
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-02-17 15:16+0100\n"
10
+ "POT-Creation-Date: 2025-03-18 12:26+0100\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:77
50
+ #: fields/minutes_duration_field.py:31
51
51
  msgid "Invalid value."
52
52
  msgstr "Valeur invalide."
53
53
 
54
- #: fields.py:92
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”."
@@ -267,6 +267,10 @@ msgstr "Un nouveau code d'authentification a été envoyé par e-mail."
267
267
  msgid "Invalid value for {filter} filter"
268
268
  msgstr "Valeur invalide pour le filtre {filter}"
269
269
 
270
+ #: views/media_rest_view_mixin.py:95 views/media_rest_view_mixin.py:143
271
+ msgid "Unexpected storage error"
272
+ msgstr "Erreur de stockage inattendue"
273
+
270
274
  #: views/ordered_rest_view_mixin.py:32
271
275
  #, python-brace-format
272
276
  msgid "object parameter is mandatory for move={move}."
@@ -277,27 +281,23 @@ msgstr "Le paramètre object est requis pour move={move}."
277
281
  msgid "object {pk} does not exists in this move context."
278
282
  msgstr "object {pk} n'existe pas dans ce contexte de déplacement."
279
283
 
280
- #: views/rest_views.py:246
284
+ #: views/rest_views.py:239
281
285
  #, python-brace-format
282
286
  msgid "{obj} cannot be deleted because it is referenced by other objects."
283
287
  msgstr ""
284
288
  "{obj} ne peut pas être supprimé car il est référencé par d’autres objets."
285
289
 
286
- #: views/rest_views.py:331
290
+ #: views/rest_views.py:324
287
291
  #, python-brace-format
288
292
  msgid "{model} {obj} created."
289
293
  msgstr "{model} {obj} créé."
290
294
 
291
- #: views/rest_views.py:332
295
+ #: views/rest_views.py:325
292
296
  #, python-brace-format
293
297
  msgid "{model} {obj} updated."
294
298
  msgstr "{model} {obj} modifié."
295
299
 
296
- #: views/rest_views.py:1177
300
+ #: views/rest_views.py:1170
297
301
  #, python-brace-format
298
302
  msgid "{model} {obj} deleted."
299
303
  msgstr "{model} {obj} supprimé."
300
-
301
- #: views/rest_views.py:1276 views/rest_views.py:1316
302
- msgid "Unexpected storage error"
303
- msgstr "Erreur de stockage inattendue"
@@ -0,0 +1,3 @@
1
+ from .exceptions import StorageException
2
+ from .local_storage import LocalStorage
3
+ from .s3_storage import S3Storage
@@ -0,0 +1,3 @@
1
+
2
+ class StorageException(Exception):
3
+ pass
@@ -0,0 +1,56 @@
1
+ import hashlib
2
+ import logging
3
+ from pathlib import Path
4
+
5
+ from django.http import FileResponse
6
+
7
+ from pfx.pfxcore.shortcuts import settings
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class LocalStorage:
13
+ direct = True
14
+
15
+ def to_python(self, value):
16
+ return value
17
+
18
+ def get_url(self, request, key):
19
+ return Path(settings.STORAGE_LOCAL_ROOT, key)
20
+
21
+ def upload(self, key, file, **kwargs):
22
+ key_list = key.split('/')
23
+ filename = key_list[-1]
24
+ relative_path = Path(*key_list[:-1])
25
+ dirname = Path(settings.STORAGE_LOCAL_ROOT, relative_path)
26
+ dirname.mkdir(parents=True, exist_ok=True)
27
+ ext = ''.join(filename.partition('.')[-2:])
28
+ hashname = f'{hashlib.sha1(file).hexdigest()}{ext}'
29
+ final_key = str(Path(relative_path, hashname))
30
+ with open(Path(dirname, hashname), 'wb') as f:
31
+ f.write(file)
32
+ with open(Path(dirname, hashname), 'rb') as f:
33
+ response = FileResponse(
34
+ f, as_attachment=True, filename=filename)
35
+ return {
36
+ 'key': final_key,
37
+ 'name': filename,
38
+ 'content-length': response.get('Content-Length'),
39
+ 'content-type': response.get('Content-Type'),
40
+ }
41
+
42
+ def delete(self, value):
43
+ if 'key' not in value:
44
+ return value # pragma: no cover
45
+ path = Path(settings.STORAGE_LOCAL_ROOT, value['key'])
46
+ path.unlink(missing_ok=True)
47
+ while True:
48
+ path = path.parent
49
+ if path == Path(settings.STORAGE_LOCAL_ROOT):
50
+ break
51
+ if any(path.iterdir()):
52
+ break
53
+ try:
54
+ path.rmdir()
55
+ except FileNotFoundError:
56
+ pass
@@ -6,14 +6,14 @@ from botocore.exceptions import ClientError
6
6
 
7
7
  from pfx.pfxcore.shortcuts import settings
8
8
 
9
- logger = logging.getLogger(__name__)
10
-
9
+ from .exceptions import StorageException
11
10
 
12
- class StorageException(Exception):
13
- pass
11
+ logger = logging.getLogger(__name__)
14
12
 
15
13
 
16
14
  class S3Storage:
15
+ direct = False
16
+
17
17
  @staticmethod
18
18
  def s3_client():
19
19
  return boto3.client(
@@ -60,15 +60,16 @@ class S3Storage:
60
60
  def upload(self, key, file, **kwargs):
61
61
  try:
62
62
  if isinstance(file, IOBase):
63
- return self.s3_client().upload_fileobj(
63
+ self.s3_client().upload_fileobj(
64
64
  file, settings.STORAGE_S3_AWS_S3_BUCKET, key,
65
65
  ExtraArgs=kwargs)
66
66
  else:
67
- return self.s3_client().upload_file(
67
+ self.s3_client().upload_file(
68
68
  file, settings.STORAGE_S3_AWS_S3_BUCKET, key,
69
69
  ExtraArgs=kwargs)
70
70
  except ClientError:
71
71
  raise StorageException
72
+ return dict(key=key)
72
73
 
73
74
  def delete(self, value):
74
75
  if 'key' not in value:
@@ -8,6 +8,7 @@ from .authentication_views import (
8
8
  from .fields import VF, FieldType, ViewField, ViewModelField
9
9
  from .filters_views import Filter, FilterGroup, ModelFilter
10
10
  from .locale_views import LocaleRestView
11
+ from .media_rest_view_mixin import MediaPermsRestViewMixin, MediaRestViewMixin
11
12
  from .ordered_rest_view_mixin import OrderedRestViewMixin
12
13
  from .rest_views import (
13
14
  BaseRestView,
@@ -20,8 +21,6 @@ from .rest_views import (
20
21
  DetailRestViewMixin,
21
22
  ListPermsRestViewMixin,
22
23
  ListRestViewMixin,
23
- MediaPermsRestViewMixin,
24
- MediaRestViewMixin,
25
24
  ModelBodyMixin,
26
25
  ModelMixin,
27
26
  ModelResponseMixin,
@@ -0,0 +1,171 @@
1
+ import logging
2
+
3
+ from django.core.exceptions import FieldDoesNotExist
4
+ from django.http import FileResponse, HttpResponse
5
+ from django.shortcuts import redirect
6
+ from django.utils.translation import gettext_lazy as _
7
+
8
+ from pfx.pfxcore.decorator import rest_api
9
+ from pfx.pfxcore.exceptions import APIError, NotFoundError
10
+ from pfx.pfxcore.fields import MediaField
11
+ from pfx.pfxcore.http import JsonResponse
12
+ from pfx.pfxcore.shortcuts import get_bool, settings
13
+ from pfx.pfxcore.storage.s3_storage import StorageException
14
+
15
+ from . import parameters
16
+ from .rest_views import ModelMixin
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class MediaRestViewMixin(ModelMixin):
22
+ """Extension mixin to manage media fields."""
23
+
24
+ def _get_model_field(self, field):
25
+ try:
26
+ model_field = self.model._meta.get_field(field)
27
+ if not isinstance(model_field, MediaField):
28
+ raise NotFoundError # pragma: no cover
29
+ except FieldDoesNotExist: # pragma: no cover
30
+ raise NotFoundError
31
+ return model_field
32
+
33
+ @rest_api(
34
+ "/<int:pk>/<str:field>/upload-url/<str:filename>", method="get",
35
+ priority_doc=-8)
36
+ def field_media_upload_url(self, pk, field, filename, *args, **kwargs):
37
+ """Entrypoint for
38
+ :code:`GET /<int:pk>/<str:field>/upload-url/<str:filename>` route.
39
+
40
+ Get the upload URL for a media file.
41
+
42
+ :param pk: The object pk
43
+ :param field: The field name
44
+ :param filename: The file name
45
+ :returns: The JSON response
46
+ :rtype: :class:`JsonResponse`
47
+ ---
48
+ get:
49
+ summary: Get upload URL
50
+ description: |
51
+ Get upload URL for a `MediaField` field.
52
+
53
+ You can upload a file ont the received URL. When the upload
54
+ query is done, you have to confirm the process with an
55
+ update request (`PUT`) on {model}. The body of this request
56
+ must contain the name of the `MediaField` with the contents
57
+ of the `file` value in the response of this request.
58
+
59
+ 1. `GET /<int:pk>/<str:field>/upload-url/<str:filename>`
60
+ → `data`
61
+ 2. `PUT data.url aFile Content-Type: aFile.type`
62
+ 3. `PUT /<int:pk> {{field: data.file}}`
63
+ parameters extras:
64
+ pk: The {model} pk.
65
+ field: The {model} field name. Must be the name of
66
+ a `MediaField` field.
67
+ filename: The desired filename.
68
+ responses:
69
+ 200:
70
+ description: The upload URL
71
+ content:
72
+ application/json:
73
+ schema:
74
+ properties:
75
+ url:
76
+ type: string
77
+ format: uri
78
+ file:
79
+ type: object
80
+ properties:
81
+ name:
82
+ type: string
83
+ key:
84
+ type: string
85
+ """
86
+ obj = self.get_object(pk=pk)
87
+ mediaField = self._get_model_field(field)
88
+ if mediaField.storage.direct:
89
+ raise APIError("Unavailable for direct storage")
90
+ try:
91
+ res = self._get_model_field(field).get_upload_url(
92
+ self.request, obj, filename)
93
+ except StorageException as e: # pragma: no cover
94
+ logger.exception(e)
95
+ raise APIError(_("Unexpected storage error", status=500))
96
+ return JsonResponse(res)
97
+
98
+ @rest_api(
99
+ "/<int:pk>/<str:field>", method="get",
100
+ parameters=[parameters.MediaRedirect], priority_doc=-9)
101
+ def field_media_get(self, pk, field, *args, **kwargs):
102
+ """Entrypoint for :code:`GET /<int:pk>/<str:field>` route.
103
+
104
+ Get the download URL for a media file.
105
+
106
+ :param pk: The object pk
107
+ :param field: The field name
108
+ :returns: The JSON response
109
+ :rtype: :class:`JsonResponse`
110
+ ---
111
+ get:
112
+ summary: Get {model} file
113
+ description: Get the URL for a media field file.
114
+ parameters extras:
115
+ pk: the {model} pk
116
+ field: the {model} field name
117
+ responses:
118
+ 200:
119
+ description: |
120
+ The file stream if storage is direct,
121
+ otherwise the file URL, only if `redirect` is `false`.
122
+ content:
123
+ application/json:
124
+ schema:
125
+ properties:
126
+ url:
127
+ type: string
128
+ format: uri
129
+ application/octet-stream:
130
+ schema:
131
+ type: file
132
+ 302:
133
+ description: |
134
+ The redirect, for undirect storage
135
+ if `redirect` is `true`.
136
+ """
137
+ obj = self.get_object(pk=pk)
138
+ mediaField = self._get_model_field(field)
139
+ try:
140
+ url = mediaField.get_url(self.request, obj)
141
+ except StorageException as e: # pragma: no cover
142
+ logger.exception(e)
143
+ raise APIError(_("Unexpected storage error", status=500))
144
+
145
+ if mediaField.storage.direct:
146
+ filename = getattr(obj, field).get('name')
147
+ if settings.STORAGE_LOCAL_X_ACCEL_REDIRECT:
148
+ response = HttpResponse()
149
+ response["Content-Disposition"] = (
150
+ f"attachment; filename={filename}")
151
+ response['X-Accel-Redirect'] = (
152
+ f"/filestore/{getattr(obj, field).get('key')}")
153
+ return response
154
+ response = FileResponse(
155
+ open(mediaField.get_url(self.request, obj), 'rb'),
156
+ as_attachment=True, filename=getattr(obj, field).get('name'))
157
+ return response
158
+
159
+ if get_bool(self.request.GET, 'redirect'):
160
+ return redirect(url)
161
+ return JsonResponse(dict(url=url))
162
+
163
+
164
+ class MediaPermsRestViewMixin(MediaRestViewMixin):
165
+ """Extension mixin to check permissions."""
166
+
167
+ def field_media_upload_url_perm(self, *args, **kwargs):
168
+ return self.request.user.has_perm(*self.get_model_perms('change'))
169
+
170
+ def field_media_get_perm(self, *args, **kwargs):
171
+ return self.request.user.has_perm(*self.get_model_perms('view'))