django-pfx 1.4.dev144__tar.gz → 1.4.dev148__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 (198) hide show
  1. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/PKG-INFO +1 -1
  2. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/django_pfx.egg-info/PKG-INFO +1 -1
  3. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/django_pfx.egg-info/SOURCES.txt +1 -0
  4. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.po +5 -5
  5. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/models/pfx_models.py +1 -0
  6. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/views/fields.py +83 -14
  7. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/views/rest_views.py +58 -25
  8. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/tests/__init__.py +1 -0
  9. django_pfx-1.4.dev148/tests/tests/test_fields_one2many.py +197 -0
  10. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/.gitignore +0 -0
  11. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/.gitlab-ci.yml +0 -0
  12. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/.pre-commit-config.yaml +0 -0
  13. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/LICENSE +0 -0
  14. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/MANIFEST.in +0 -0
  15. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/README.md +0 -0
  16. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/django_pfx.egg-info/dependency_links.txt +0 -0
  17. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/django_pfx.egg-info/requires.txt +0 -0
  18. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/django_pfx.egg-info/top_level.txt +0 -0
  19. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/doc/Makefile +0 -0
  20. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/doc/conf.py +0 -0
  21. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/doc/index.rst +0 -0
  22. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/doc/source/api.views.rst +0 -0
  23. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/doc/source/authentication.md +0 -0
  24. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/doc/source/decorator.md +0 -0
  25. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/doc/source/generate_openapi.md +0 -0
  26. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/doc/source/getting_started.md +0 -0
  27. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/doc/source/internationalisation.md +0 -0
  28. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/doc/source/model.md +0 -0
  29. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/doc/source/pfx_views.md +0 -0
  30. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/doc/source/profiling.md +0 -0
  31. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/doc/source/settings.md +0 -0
  32. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/doc/source/testing.md +0 -0
  33. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/img/pfx.png +0 -0
  34. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/img/pfx.svg +0 -0
  35. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/make_messages +0 -0
  36. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/manage.py +0 -0
  37. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/__init__.py +0 -0
  38. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/__init__.py +0 -0
  39. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/apidoc/__init__.py +0 -0
  40. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/apidoc/parameters.py +0 -0
  41. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/apidoc/schema.py +0 -0
  42. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/apidoc/tags.py +0 -0
  43. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/apps.py +0 -0
  44. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/decorator/__init__.py +0 -0
  45. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/decorator/rest.py +0 -0
  46. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/default_settings.py +0 -0
  47. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/exceptions.py +0 -0
  48. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/fields/__init__.py +0 -0
  49. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/fields/decimal_field.py +0 -0
  50. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/fields/media_field.py +0 -0
  51. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/fields/minutes_duration_field.py +0 -0
  52. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/fields/nh3_field.py +0 -0
  53. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/http/__init__.py +0 -0
  54. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/http/json_response.py +0 -0
  55. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
  56. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/management/__init__.py +0 -0
  57. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/management/commands/__init__.py +0 -0
  58. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/management/commands/makeapidoc.py +0 -0
  59. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/management/commands/profile.py +0 -0
  60. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/middleware/__init__.py +0 -0
  61. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/middleware/authentication.py +0 -0
  62. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/middleware/locale.py +0 -0
  63. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/middleware/profiling.py +0 -0
  64. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/migrations/0001_initial.py +0 -0
  65. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/migrations/0002_pfxpermissionsuser.py +0 -0
  66. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/migrations/0003_delete_pfxpermissionsuser.py +0 -0
  67. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/migrations/__init__.py +0 -0
  68. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/migrations/operations/__init__.py +0 -0
  69. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/migrations/operations/permissions.py +0 -0
  70. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/models/__init__.py +0 -0
  71. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/models/abstract_pfx_base_user.py +0 -0
  72. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/models/cache_mixins.py +0 -0
  73. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/models/login_ban.py +0 -0
  74. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/models/not_null_fields.py +0 -0
  75. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/models/ordered_model_mixin.py +0 -0
  76. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/models/otp_user_mixin.py +0 -0
  77. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/models/pfx_user.py +0 -0
  78. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/models/user_filtered_queryset_mixin.py +0 -0
  79. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/serializers/__init__.py +0 -0
  80. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/serializers/json.py +0 -0
  81. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/settings.py +0 -0
  82. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/shortcuts.py +0 -0
  83. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/storage/__init__.py +0 -0
  84. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/storage/exceptions.py +0 -0
  85. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/storage/local_storage.py +0 -0
  86. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/storage/s3_storage.py +0 -0
  87. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/templates/registration/otp_code_email.txt +0 -0
  88. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/templates/registration/otp_code_subject.txt +0 -0
  89. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/templates/registration/password_reset_email.txt +0 -0
  90. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/templates/registration/password_reset_subject.txt +0 -0
  91. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/templates/registration/welcome_email.txt +0 -0
  92. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/templates/registration/welcome_subject.txt +0 -0
  93. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/test.py +0 -0
  94. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/urls.py +0 -0
  95. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/views/__init__.py +0 -0
  96. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/views/authentication_views.py +0 -0
  97. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/views/filters_views.py +0 -0
  98. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/views/locale_views.py +0 -0
  99. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/views/media_rest_view_mixin.py +0 -0
  100. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/views/ordered_rest_view_mixin.py +0 -0
  101. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/views/parameters/__init__.py +0 -0
  102. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/views/parameters/date_format.py +0 -0
  103. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/views/parameters/groups.py +0 -0
  104. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/views/parameters/list_count.py +0 -0
  105. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/views/parameters/list_items.py +0 -0
  106. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/views/parameters/list_mode.py +0 -0
  107. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/views/parameters/list_order.py +0 -0
  108. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/views/parameters/list_search.py +0 -0
  109. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/views/parameters/media_redirect.py +0 -0
  110. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/views/parameters/meta_fields.py +0 -0
  111. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/views/parameters/meta_filters.py +0 -0
  112. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/views/parameters/meta_orders.py +0 -0
  113. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/views/parameters/subset.py +0 -0
  114. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/views/parameters/subset_limit.py +0 -0
  115. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/views/parameters/subset_offset.py +0 -0
  116. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/views/parameters/subset_page.py +0 -0
  117. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/views/parameters/subset_page_size.py +0 -0
  118. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/pfxcore/views/parameters/subset_page_subset.py +0 -0
  119. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/settings/__init__.py +0 -0
  120. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pfx/settings/dev.py +0 -0
  121. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/pyproject.toml +0 -0
  122. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/requirements.txt +0 -0
  123. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/serve-doc +0 -0
  124. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/setup.cfg +0 -0
  125. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/setup.py +0 -0
  126. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/__init__.py +0 -0
  127. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/apps.py +0 -0
  128. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/locale/fr/LC_MESSAGES/django.po +0 -0
  129. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/migrations/0001_initial.py +0 -0
  130. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/migrations/0002_alter_book_cover.py +0 -0
  131. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/migrations/0003_book_local_file.py +0 -0
  132. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/migrations/__init__.py +0 -0
  133. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/models.py +0 -0
  134. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/settings/__init__.py +0 -0
  135. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/settings/ci.py +0 -0
  136. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/settings/common.py +0 -0
  137. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/settings/dev.py +0 -0
  138. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/settings/dev_custom_example.py +0 -0
  139. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/settings/dev_default.py +0 -0
  140. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/tests/basic_api_errors.py +0 -0
  141. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/tests/basic_api_test.py +0 -0
  142. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/tests/test_api_doc.py +0 -0
  143. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/tests/test_api_doc_search.py +0 -0
  144. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/tests/test_auth_api.py +0 -0
  145. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/tests/test_body_mixin.py +0 -0
  146. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/tests/test_cache.py +0 -0
  147. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/tests/test_client.py +0 -0
  148. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/tests/test_fields_choices.py +0 -0
  149. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/tests/test_fields_date.py +0 -0
  150. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/tests/test_fields_decimal.py +0 -0
  151. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/tests/test_fields_minutes_duration.py +0 -0
  152. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/tests/test_fields_nh3.py +0 -0
  153. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/tests/test_filters.py +0 -0
  154. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/tests/test_locale_api.py +0 -0
  155. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/tests/test_ordered_rest_view_mixin.py +0 -0
  156. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/tests/test_perm_tests.py +0 -0
  157. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/tests/test_permissions.py +0 -0
  158. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/tests/test_perms_api.py +0 -0
  159. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/tests/test_post_migrate_groups_update.py +0 -0
  160. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/tests/test_profiling_middleware.py +0 -0
  161. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/tests/test_settings.py +0 -0
  162. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/tests/test_shortcuts.py +0 -0
  163. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/tests/test_timezone_middleware.py +0 -0
  164. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/tests/test_tools.py +0 -0
  165. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/tests/test_user_queryset.py +0 -0
  166. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/tests/test_view_decorators.py +0 -0
  167. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/tests/test_view_fields.py +0 -0
  168. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/urls.py +0 -0
  169. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests/views.py +0 -0
  170. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests_base_user/__init__.py +0 -0
  171. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests_base_user/migrations/0001_initial.py +0 -0
  172. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests_base_user/migrations/__init__.py +0 -0
  173. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests_base_user/settings/__init__.py +0 -0
  174. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests_base_user/settings/ci.py +0 -0
  175. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests_base_user/settings/common.py +0 -0
  176. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests_base_user/settings/dev.py +0 -0
  177. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests_base_user/settings/dev_custom_example.py +0 -0
  178. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests_base_user/settings/dev_default.py +0 -0
  179. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests_base_user/tests/__init__.py +0 -0
  180. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests_base_user/tests/test_api.py +0 -0
  181. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests_base_user/tests/test_auth_api.py +0 -0
  182. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests_base_user/urls.py +0 -0
  183. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests_base_user/views.py +0 -0
  184. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests_custom_user/__init__.py +0 -0
  185. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests_custom_user/migrations/0001_initial.py +0 -0
  186. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests_custom_user/migrations/__init__.py +0 -0
  187. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests_custom_user/models.py +0 -0
  188. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests_custom_user/settings/__init__.py +0 -0
  189. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests_custom_user/settings/ci.py +0 -0
  190. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests_custom_user/settings/common.py +0 -0
  191. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests_custom_user/settings/dev.py +0 -0
  192. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests_custom_user/settings/dev_custom_example.py +0 -0
  193. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests_custom_user/settings/dev_default.py +0 -0
  194. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests_custom_user/tests/__init__.py +0 -0
  195. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests_custom_user/tests/test_api.py +0 -0
  196. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests_custom_user/tests/test_auth_api.py +0 -0
  197. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/tests_custom_user/urls.py +0 -0
  198. {django_pfx-1.4.dev144 → django_pfx-1.4.dev148}/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.4.dev144
3
+ Version: 1.4.dev148
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.4.dev144
3
+ Version: 1.4.dev148
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
@@ -150,6 +150,7 @@ tests/tests/test_fields_date.py
150
150
  tests/tests/test_fields_decimal.py
151
151
  tests/tests/test_fields_minutes_duration.py
152
152
  tests/tests/test_fields_nh3.py
153
+ tests/tests/test_fields_one2many.py
153
154
  tests/tests/test_filters.py
154
155
  tests/tests/test_locale_api.py
155
156
  tests/tests/test_ordered_rest_view_mixin.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: 2025-09-09 10:06+0200\n"
10
+ "POT-Creation-Date: 2025-09-15 14:06+0200\n"
11
11
  "PO-Revision-Date: 2021-06-22 23:31+0200\n"
12
12
  "Last-Translator: \n"
13
13
  "Language-Team: \n"
@@ -286,23 +286,23 @@ msgstr "Le paramètre object est requis pour move={move}."
286
286
  msgid "object {pk} does not exists in this move context."
287
287
  msgstr "object {pk} n'existe pas dans ce contexte de déplacement."
288
288
 
289
- #: views/rest_views.py:234
289
+ #: views/rest_views.py:220
290
290
  #, python-brace-format
291
291
  msgid "{obj} cannot be deleted because it is referenced by other objects."
292
292
  msgstr ""
293
293
  "{obj} ne peut pas être supprimé car il est référencé par d’autres objets."
294
294
 
295
- #: views/rest_views.py:331
295
+ #: views/rest_views.py:344
296
296
  #, python-brace-format
297
297
  msgid "{model} {obj} created."
298
298
  msgstr "{model} {obj} créé."
299
299
 
300
- #: views/rest_views.py:332
300
+ #: views/rest_views.py:345
301
301
  #, python-brace-format
302
302
  msgid "{model} {obj} updated."
303
303
  msgstr "{model} {obj} modifié."
304
304
 
305
- #: views/rest_views.py:1184
305
+ #: views/rest_views.py:1217
306
306
  #, python-brace-format
307
307
  msgid "{model} {obj} deleted."
308
308
  msgstr "{model} {obj} supprimé."
@@ -29,6 +29,7 @@ class JSONReprMixin():
29
29
 
30
30
  def __init__(self, *args, **kw):
31
31
  super().__init__(*args, **kw)
32
+ self._rel_data = {}
32
33
  self._after_save = []
33
34
 
34
35
  def get_url(self):
@@ -93,12 +93,23 @@ class FieldType:
93
93
  return cls.APIDOC_FIELD_BINDING.get(field_type)
94
94
 
95
95
 
96
+ def get_db_type(field):
97
+ if isinstance(field, models.ManyToManyRel):
98
+ return "ManyToManyField"
99
+ elif isinstance(field, models.ManyToOneRel):
100
+ return "OneToManyField"
101
+ elif isinstance(field, models.OneToOneRel):
102
+ return "OneToOneField"
103
+ return field.__class__.__name__
104
+
105
+
96
106
  class ViewField:
97
107
  def __init__(
98
- self, name, verbose_name=None, alias=None, field_type=None,
108
+ self, name, verbose_name=None, alias=None,
109
+ field_type=None, db_type=None,
99
110
  readonly=False, readonly_create=False, readonly_update=False,
100
- choices=None, json_repr=None,
101
- related_model=None, related_model_api=None,
111
+ choices=None, json_repr=None, media_field_api=None,
112
+ related_model=None, related_model_api=None, related_fields=None,
102
113
  select_related=None, prefetch_related=None):
103
114
  self.name = name
104
115
  self.alias = alias or name
@@ -106,10 +117,16 @@ class ViewField:
106
117
  self.readonly_update = readonly or readonly_update
107
118
  self.verbose_name = verbose_name or name
108
119
  self.field_type = field_type
120
+ self.db_type = db_type
109
121
  self.choices = dict(choices or [])
110
122
  self.json_repr = json_repr
123
+ self.media_field_api = media_field_api
111
124
  self.related_model = related_model
112
125
  self.related_model_api = related_model_api
126
+ self.related_fields = None
127
+ if self.related_model and related_fields:
128
+ self.related_fields = process_fields(
129
+ self.related_model, related_fields)
113
130
  self.select_related = select_related or []
114
131
  self.prefetch_related = prefetch_related or []
115
132
  self.model_field = None
@@ -118,7 +135,8 @@ class ViewField:
118
135
  return self.readonly_create if created else self.readonly_update
119
136
 
120
137
  def meta(self):
121
- res = dict(type=self.field_type, name=self.verbose_name)
138
+ res = dict(
139
+ type=self.field_type, db_type=self.db_type, name=self.verbose_name)
122
140
  if self.choices:
123
141
  res['choices'] = [
124
142
  dict(label=str(v), value=k) for k, v in self.choices.items()]
@@ -126,6 +144,9 @@ class ViewField:
126
144
  res['model'] = self.related_model._meta.label
127
145
  res['api'] = self.related_model_api or getattr(
128
146
  self.related_model, 'api', None)
147
+ if self.related_fields:
148
+ res['fields'] = {
149
+ n: f.meta() for n, f in self.related_fields.items()}
129
150
  res['readonly'] = dict(
130
151
  post=self.readonly_create,
131
152
  put=self.readonly_update)
@@ -144,27 +165,39 @@ class ViewField:
144
165
  return None
145
166
  return getattr(obj, name)
146
167
 
147
- def _json_repr(self, value):
168
+ def _related_json_repr(self, value):
148
169
  if not value:
149
170
  return None
150
171
  if self.json_repr:
151
- return self.json_repr(value)
152
- return value.json_repr()
172
+ vals = self.json_repr(value)
173
+ else:
174
+ vals = value.json_repr()
175
+ if self.related_fields:
176
+ vals.update({
177
+ n: f.to_json(value) for n, f in self.related_fields.items()})
178
+ return vals
153
179
 
154
- def to_json(self, view, obj):
180
+ def to_json(self, obj, view=None):
155
181
  value = self.get_value(obj)
156
182
 
157
183
  if self.field_type == FieldType.ModelObject:
158
- return self._json_repr(value)
184
+ return self._related_json_repr(value)
159
185
  if self.field_type == FieldType.ModelObjectList:
160
- return [self._json_repr(o) for o in value.all()]
186
+ return [self._related_json_repr(o) for o in value.all()]
161
187
 
162
188
  if self.json_repr:
163
189
  return self.json_repr(value)
164
190
 
165
191
  if self.field_type == FieldType.MediaField:
166
192
  if value:
167
- api_url = view._rest_view_path
193
+ if self.media_field_api:
194
+ api_url = self.media_field_api
195
+ elif view:
196
+ api_url = view._rest_view_path
197
+ else:
198
+ raise Exception(
199
+ "media_field_api must be defined if the field "
200
+ "is not a view root field.")
168
201
  value['url'] = f'{api_url}/{obj.pk}/{self.name}'
169
202
  return value
170
203
  else:
@@ -186,12 +219,15 @@ class ViewField:
186
219
  verbose_name = getattr(
187
220
  prop.fget, 'short_description', prop.fget.__name__)
188
221
  field_type = getattr(prop.fget, 'field_type', None)
222
+ db_type = getattr(prop.fget, 'db_type', None)
189
223
  else:
190
224
  verbose_name = (
191
225
  hasattr(prop, 'name') and prop.name or str(prop))
192
226
  field_type = None
227
+ db_type = None
193
228
  setifnone(kwargs, 'verbose_name', verbose_name)
194
229
  setifnone(kwargs, 'field_type', field_type)
230
+ setifnone(kwargs, 'db_type', db_type)
195
231
  return ViewField(name, **kwargs)
196
232
 
197
233
  @classmethod
@@ -199,6 +235,8 @@ class ViewField:
199
235
  setifnone(kwargs, 'verbose_name', cls._get_model_verbose_name(field))
200
236
  setifnone(
201
237
  kwargs, 'field_type', FieldType.from_model_field(field.__class__))
238
+ setifnone(
239
+ kwargs, 'db_type', get_db_type(field))
202
240
  return ViewModelField(
203
241
  name, model_field=field, **kwargs)
204
242
 
@@ -317,6 +355,7 @@ class ViewField:
317
355
  class ViewModelField(ViewField):
318
356
  def __init__(
319
357
  self, name, model_field=None, **kwargs):
358
+ related_fields = kwargs.get('related_fields')
320
359
  super().__init__(name, **kwargs)
321
360
  self.model_field = model_field
322
361
  if hasattr(model_field, 'to_json'):
@@ -324,6 +363,9 @@ class ViewModelField(ViewField):
324
363
  if (hasattr(self.model_field, 'related_model') and
325
364
  self.model_field.related_model and not self.related_model):
326
365
  self.related_model = self.model_field.related_model
366
+ if self.related_model and related_fields:
367
+ self.related_fields = process_fields(
368
+ self.related_model, related_fields)
327
369
  if not self.select_related and (
328
370
  self.field_type == FieldType.ModelObject):
329
371
  # Auto add the field in select_related if select_related
@@ -350,7 +392,13 @@ class ViewModelField(ViewField):
350
392
 
351
393
  def to_model_value(self, value, get_related_queryset):
352
394
  def _get_obj(v):
353
- pk = v['pk'] if isinstance(v, dict) and 'pk' in v else v
395
+ if isinstance(v, dict):
396
+ if 'pk' in v:
397
+ pk = v['pk']
398
+ else:
399
+ return self.model_field.related_model()
400
+ else:
401
+ pk = v
354
402
  return pk and get_object(
355
403
  get_related_queryset(self.model_field.related_model),
356
404
  related_field=self.name, pk=pk) or None
@@ -397,7 +445,8 @@ class VF:
397
445
  readonly=False, readonly_create=False, readonly_update=False,
398
446
  choices=None, select_related=None, prefetch_related=None,
399
447
  json_repr=None, related_model=None, related_model_api=None,
400
- field=None):
448
+ field=None, related_fields=None, media_field_api=None,
449
+ db_type=None):
401
450
  self.kwargs = dict(
402
451
  name=name, verbose_name=verbose_name, field_type=field_type,
403
452
  alias=alias,
@@ -405,7 +454,27 @@ class VF:
405
454
  readonly_update=readonly_update, choices=choices,
406
455
  select_related=select_related, prefetch_related=prefetch_related,
407
456
  json_repr=json_repr, related_model=related_model,
408
- related_model_api=related_model_api, field=field)
457
+ related_model_api=related_model_api, field=field,
458
+ related_fields=related_fields, media_field_api=media_field_api,
459
+ db_type=db_type)
409
460
 
410
461
  def to_field(self, model):
411
462
  return ViewField.from_name(model, **self.kwargs)
463
+
464
+
465
+ def process_fields(model, fields):
466
+ if not fields:
467
+ return {
468
+ _f.name: ViewField.from_model_field(_f.name, _f)
469
+ for _f in model._meta.fields}
470
+
471
+ def _field(e):
472
+ if isinstance(e, ViewField):
473
+ field = e
474
+ elif isinstance(e, VF):
475
+ field = e.to_field(model)
476
+ else:
477
+ field = ViewField.from_name(model, e)
478
+ return field.alias, field
479
+
480
+ return dict(_field(e) for e in fields)
@@ -7,7 +7,7 @@ from django.conf import settings
7
7
  from django.core.cache import cache
8
8
  from django.core.exceptions import FieldError, ValidationError
9
9
  from django.db import IntegrityError, transaction
10
- from django.db.models import ForeignKey, Model, Q
10
+ from django.db.models import ForeignKey, ManyToOneRel, Model, Q
11
11
  from django.db.models.fields import AutoFieldMixin
12
12
  from django.urls import path
13
13
  from django.utils.translation import gettext_lazy as _
@@ -36,11 +36,11 @@ from pfx.pfxcore.shortcuts import (
36
36
  get_object,
37
37
  model_permissions,
38
38
  )
39
+ from pfx.pfxcore.views.fields import ModelList
39
40
 
40
41
  from . import parameters
41
- from .fields import VF
42
42
  from .fields import FieldType as FT
43
- from .fields import ViewField
43
+ from .fields import process_fields
44
44
 
45
45
  logger = logging.getLogger(__name__)
46
46
 
@@ -145,21 +145,7 @@ class ModelMixin():
145
145
 
146
146
  @classmethod
147
147
  def _process_fields(cls, fields):
148
- if not fields:
149
- return {
150
- _f.name: ViewField.from_model_field(_f.name, _f)
151
- for _f in cls.model._meta.fields}
152
-
153
- def _field(e):
154
- if isinstance(e, ViewField):
155
- field = e
156
- elif isinstance(e, VF):
157
- field = e.to_field(cls.model)
158
- else:
159
- field = ViewField.from_name(cls.model, e)
160
- return field.alias, field
161
-
162
- return dict(_field(e) for e in fields)
148
+ return process_fields(cls.model, fields)
163
149
 
164
150
  @classmethod
165
151
  def get_fields(cls):
@@ -303,7 +289,7 @@ class ModelResponseMixin(ModelMixin):
303
289
  :rtype: :class:`JsonResponse`
304
290
  """
305
291
  return JsonResponse(self.serialize_object(o, **{
306
- _f.alias: _f.to_json(self, o)
292
+ _f.alias: _f.to_json(o, self)
307
293
  for _f in self.get_fields().values()}, meta=meta))
308
294
 
309
295
  def validate(self, obj, rel_data=None, created=False, **kwargs):
@@ -316,7 +302,34 @@ class ModelResponseMixin(ModelMixin):
316
302
  :param created: If object instance is created
317
303
  :param \\**kwargs: Additional arguments for :code:`full_clean`
318
304
  """
319
- obj.full_clean(**kwargs)
305
+ def model_lists(rel_data):
306
+ if rel_data:
307
+ for k, v in rel_data.items():
308
+ if isinstance(v, ModelList):
309
+ yield k, v
310
+
311
+ errors = {}
312
+ for k, v in model_lists(rel_data):
313
+ for i, o in enumerate(v):
314
+ save_field = getattr(o, '_save_related', False)
315
+ if save_field:
316
+ try:
317
+ o.full_clean(exclude={save_field})
318
+ except ValidationError as e:
319
+ for mk, ms in e.error_dict.items():
320
+ errors.setdefault(
321
+ f'{k}::{i}::{mk}', []).extend(ms)
322
+ obj._rel_data = rel_data or {}
323
+ if errors:
324
+ try:
325
+ obj._rel_data = rel_data
326
+ obj.full_clean(**kwargs)
327
+ except ValidationError as e:
328
+ for k, ms in e.error_dict.items():
329
+ errors.setdefault(k, []).extend(ms)
330
+ raise ValidationError(errors)
331
+ else:
332
+ obj.full_clean(**kwargs)
320
333
 
321
334
  def is_valid_response_meta(self, obj, created=False):
322
335
  """Prepare the defaut meta for is_valid responce.
@@ -356,6 +369,16 @@ class ModelResponseMixin(ModelMixin):
356
369
  obj.save()
357
370
  if rel_data:
358
371
  for k, v in rel_data.items():
372
+ if isinstance(v, ModelList):
373
+ if isinstance(obj._meta.get_field(k), ManyToOneRel):
374
+ for cur in getattr(obj, k).all():
375
+ if cur not in v:
376
+ cur.delete()
377
+ for o in v:
378
+ save_field = getattr(o, '_save_related', False)
379
+ if save_field:
380
+ setattr(o, save_field, obj)
381
+ o.save()
359
382
  getattr(obj, k).set(v)
360
383
  self.post_save(obj, created=created, funcs=funcs)
361
384
  obj = self.get_object(pk=obj.pk)
@@ -499,7 +522,7 @@ class ModelBodyMixin(BodyMixin, ModelMixin):
499
522
  """
500
523
  fields = self.get_fields()
501
524
 
502
- def can_write(fname):
525
+ def can_write(fname, fields):
503
526
  if fname not in fields:
504
527
  return False
505
528
  if fields[fname].is_readonly(created=created):
@@ -512,9 +535,19 @@ class ModelBodyMixin(BodyMixin, ModelMixin):
512
535
  res = {}
513
536
  res_rel = {}
514
537
  for k, v in data.items():
515
- if can_write(k):
516
- mk, mv = fields[k].to_model_value(v, self.get_related_queryset)
517
- if fields[k].field_type == FT.ModelObjectList:
538
+ if can_write(k, fields):
539
+ field = fields[k]
540
+ mk, mv = field.to_model_value(v, self.get_related_queryset)
541
+ if field.field_type == FT.ModelObjectList:
542
+ if field.related_fields:
543
+ for i, rv in enumerate(v):
544
+ for rk, rf in field.related_fields.items():
545
+ rmk, rmv = rf.to_model_value(
546
+ rv.get(rk), self.get_related_queryset)
547
+ setattr(mv[i], rmk, rmv)
548
+ setattr(
549
+ mv[i], '_save_related',
550
+ field.model_field.field.name)
518
551
  res_rel[mk] = mv
519
552
  else:
520
553
  res[mk] = mv
@@ -768,7 +801,7 @@ class ListRestViewMixin(ModelResponseMixin):
768
801
  *self.get_list_fields_prefetch_related())
769
802
  for o in qs:
770
803
  yield self.serialize_object(o, **{
771
- _f.alias: _f.to_json(self, o)
804
+ _f.alias: _f.to_json(o, self)
772
805
  for _f in self.get_list_fields().values()})
773
806
 
774
807
  def get_short_list_result(self, qs):
@@ -11,6 +11,7 @@ from .test_fields_date import TestFieldsDate
11
11
  from .test_fields_decimal import TestFieldsDecimal
12
12
  from .test_fields_minutes_duration import TestFieldsMinutesDuration
13
13
  from .test_fields_nh3 import TestFieldsNh3
14
+ from .test_fields_one2many import TestFieldsOne2Many
14
15
  from .test_filters import FiltersTest
15
16
  from .test_locale_api import LocaleAPITest
16
17
  from .test_ordered_rest_view_mixin import TestOrderedRestViewMixin
@@ -0,0 +1,197 @@
1
+ from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
2
+ from django.db import connection, models
3
+ from django.test import TestCase
4
+ from django.test.utils import override_settings
5
+ from django.urls import include, path
6
+
7
+ from pfx.pfxcore import register_views
8
+ from pfx.pfxcore.decorator import rest_view
9
+ from pfx.pfxcore.models import JSONReprMixin
10
+ from pfx.pfxcore.test import APIClient, TestAssertMixin
11
+ from pfx.pfxcore.views import VF, RestView
12
+ from tests.views import FakeViewMixin
13
+
14
+
15
+ class TestOne2ManyModel(JSONReprMixin, models.Model):
16
+ name = models.CharField(max_length=30)
17
+
18
+ class Meta:
19
+ verbose_name = "TestOne2ManyModel"
20
+ verbose_name_plural = "TestOne2ManyModel"
21
+ ordering = ['pk']
22
+
23
+ def clean_fields(self, exclude=None):
24
+ errors = {}
25
+
26
+ if 'rels' not in exclude and 'rels' in self._rel_data:
27
+ if len(self._rel_data['rels']) < 2:
28
+ errors.setdefault(NON_FIELD_ERRORS, []).append(
29
+ "You have to set minimum 2 rels.")
30
+
31
+ try:
32
+ super().clean_fields(exclude)
33
+ except ValidationError as e:
34
+ for k, ms in e.error_dict.items():
35
+ errors.setdefault(k, []).extend(ms)
36
+
37
+ if errors:
38
+ raise ValidationError(errors)
39
+
40
+
41
+ class TestOne2ManyRelModel(JSONReprMixin, models.Model):
42
+ name = models.CharField(max_length=30)
43
+ descr = models.TextField()
44
+ rel = models.ForeignKey(
45
+ TestOne2ManyModel, related_name='rels', on_delete=models.CASCADE)
46
+
47
+ class Meta:
48
+ verbose_name = "TestOne2ManyRelModel"
49
+ verbose_name_plural = "TestOne2ManyRelModels"
50
+ ordering = ['pk']
51
+
52
+
53
+ @rest_view("/test")
54
+ class TestOne2ManyRestView(FakeViewMixin, RestView):
55
+ default_public = True
56
+ model = TestOne2ManyModel
57
+
58
+ fields = ['name', VF('rels', related_fields=[
59
+ 'name', VF('descr', readonly=True)])]
60
+
61
+
62
+ urlpatterns = [
63
+ path('api/', include(register_views(TestOne2ManyRestView))),
64
+ path('api/', include('pfx.pfxcore.urls'))
65
+ ]
66
+
67
+
68
+ @override_settings(ROOT_URLCONF=__name__)
69
+ class TestFieldsOne2Many(TestAssertMixin, TestCase):
70
+
71
+ def setUp(self):
72
+ self.client = APIClient(default_locale='en')
73
+
74
+ @classmethod
75
+ def setUpTestData(cls):
76
+ with connection.schema_editor() as schema_editor:
77
+ schema_editor.create_model(TestOne2ManyModel)
78
+ schema_editor.create_model(TestOne2ManyRelModel)
79
+
80
+ def test_meta(self):
81
+ response = self.client.get('/api/test/meta')
82
+ self.assertRC(response, 200)
83
+ fields = 'fields.rels.fields'
84
+ self.assertJE(response, f'{fields}.name.name', "name")
85
+ self.assertJE(response, f'{fields}.name.type', "CharField")
86
+ self.assertJE(response, f'{fields}.name.required', True)
87
+ self.assertJE(response, f'{fields}.name.readonly.post', False)
88
+ self.assertJE(response, f'{fields}.name.readonly.put', False)
89
+ self.assertJE(response, f'{fields}.descr.name', "descr")
90
+ self.assertJE(response, f'{fields}.descr.type', "TextField")
91
+ self.assertJE(response, f'{fields}.descr.required', True)
92
+ self.assertJE(response, f'{fields}.descr.readonly.post', True)
93
+ self.assertJE(response, f'{fields}.descr.readonly.put', True)
94
+
95
+ def test_get(self):
96
+ o = TestOne2ManyModel.objects.create(name="Test")
97
+ r1 = TestOne2ManyRelModel.objects.create(rel=o, name="R1", descr="D1")
98
+ r2 = TestOne2ManyRelModel.objects.create(rel=o, name="R2", descr="D2")
99
+
100
+ response = self.client.get(f'/api/test/{o.pk}')
101
+ self.assertRC(response, 200)
102
+ self.assertSize(response, 'rels', 2)
103
+ self.assertJE(response, 'rels.@0.pk', r1.pk)
104
+ self.assertJE(response, 'rels.@0.name', "R1")
105
+ self.assertJE(response, 'rels.@0.descr', "D1")
106
+ self.assertJE(response, 'rels.@1.pk', r2.pk)
107
+ self.assertJE(response, 'rels.@1.name', "R2")
108
+ self.assertJE(response, 'rels.@1.descr', "D2")
109
+
110
+ def test_post(self):
111
+ response = self.client.post('/api/test', dict(
112
+ name="Test",
113
+ rels=[dict(name="R1", descr="D1"), dict(name="R2", descr="D2")]
114
+ ))
115
+ self.assertRC(response, 200)
116
+ self.assertSize(response, 'rels', 2)
117
+ self.assertJE(response, 'rels.@0.name', "R1")
118
+ self.assertJE(response, 'rels.@0.descr', "D1")
119
+ self.assertJE(response, 'rels.@1.name', "R2")
120
+ self.assertJE(response, 'rels.@1.descr', "D2")
121
+
122
+ def test_post_field_errors(self):
123
+ response = self.client.post('/api/test', dict(
124
+ rels=[dict(name="R1", descr="D1"), dict(descr="D2")]
125
+ ))
126
+ self.assertRC(response, 422)
127
+ self.assertEqual(
128
+ set(response.json().keys()), {'name', 'rels::1::name'})
129
+ self.assertJE(response, 'name', ["This field cannot be blank."])
130
+ self.assertJE(
131
+ response, 'rels::1::name', ["This field cannot be null."])
132
+
133
+ response = self.client.post('/api/test', dict(
134
+ rels=[dict(descr="D1")]
135
+ ))
136
+ self.assertRC(response, 422)
137
+ self.assertEqual(
138
+ set(response.json().keys()), {'__all__', 'name', 'rels::0::name'})
139
+ self.assertJE(response, '__all__', ["You have to set minimum 2 rels."])
140
+ self.assertJE(response, 'name', ["This field cannot be blank."])
141
+ self.assertJE(
142
+ response, 'rels::0::name', ["This field cannot be null."])
143
+
144
+ def test_put(self):
145
+ o = TestOne2ManyModel.objects.create(name="Test")
146
+ r1 = TestOne2ManyRelModel.objects.create(rel=o, name="R1", descr="D1")
147
+
148
+ vals = self.client.get(f'/api/test/{o.pk}').json()
149
+ vals['rels'][0]['name'] = "R1 updated"
150
+ vals['rels'].append({'name': "R2", "descr": "D2"})
151
+
152
+ response = self.client.put(f'/api/test/{o.pk}', vals)
153
+ self.assertRC(response, 200)
154
+ self.assertSize(response, 'rels', 2)
155
+ self.assertJE(response, 'rels.@0.pk', r1.pk)
156
+ self.assertJE(response, 'rels.@0.name', "R1 updated")
157
+ self.assertJE(response, 'rels.@0.descr', "D1")
158
+ self.assertJE(response, 'rels.@1.name', "R2")
159
+ self.assertJE(response, 'rels.@1.descr', "D2")
160
+
161
+ def test_put_field_errors(self):
162
+ o = TestOne2ManyModel.objects.create(name="Test")
163
+ TestOne2ManyRelModel.objects.create(rel=o, name="R1", descr="D1")
164
+ TestOne2ManyRelModel.objects.create(rel=o, name="R2", descr="D2")
165
+
166
+ vals = self.client.get(f'/api/test/{o.pk}').json()
167
+ vals['name'] = ""
168
+ vals['rels'][0]['name'] = None
169
+ del vals['rels'][1]
170
+
171
+ response = self.client.put(f'/api/test/{o.pk}', vals)
172
+ self.assertRC(response, 422)
173
+ self.assertEqual(
174
+ set(response.json().keys()), {'__all__', 'name', 'rels::0::name'})
175
+ self.assertJE(response, '__all__', ["You have to set minimum 2 rels."])
176
+ self.assertJE(response, 'name', ["This field cannot be blank."])
177
+ self.assertJE(
178
+ response, 'rels::0::name', ["This field cannot be null."])
179
+
180
+ def test_put_delete(self):
181
+ o = TestOne2ManyModel.objects.create(name="Test")
182
+ r1 = TestOne2ManyRelModel.objects.create(rel=o, name="R1", descr="D1")
183
+ r2 = TestOne2ManyRelModel.objects.create(rel=o, name="R2", descr="D2")
184
+ TestOne2ManyRelModel.objects.create(rel=o, name="R3", descr="D3")
185
+
186
+ vals = self.client.get(f'/api/test/{o.pk}').json()
187
+ del vals['rels'][2]
188
+
189
+ response = self.client.put(f'/api/test/{o.pk}', vals)
190
+ self.assertRC(response, 200)
191
+ self.assertSize(response, 'rels', 2)
192
+ self.assertJE(response, 'rels.@0.pk', r1.pk)
193
+ self.assertJE(response, 'rels.@0.name', "R1")
194
+ self.assertJE(response, 'rels.@0.descr', "D1")
195
+ self.assertJE(response, 'rels.@1.pk', r2.pk)
196
+ self.assertJE(response, 'rels.@1.name', "R2")
197
+ self.assertJE(response, 'rels.@1.descr', "D2")
File without changes