django-pfx 1.4.dev144__tar.gz → 1.4.dev146__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.dev146}/PKG-INFO +1 -1
  2. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/django_pfx.egg-info/PKG-INFO +1 -1
  3. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/django_pfx.egg-info/SOURCES.txt +1 -0
  4. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.po +5 -5
  5. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/models/pfx_models.py +1 -0
  6. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/views/fields.py +61 -12
  7. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/views/rest_views.py +53 -24
  8. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/tests/__init__.py +1 -0
  9. django_pfx-1.4.dev146/tests/tests/test_fields_one2many.py +178 -0
  10. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/.gitignore +0 -0
  11. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/.gitlab-ci.yml +0 -0
  12. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/.pre-commit-config.yaml +0 -0
  13. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/LICENSE +0 -0
  14. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/MANIFEST.in +0 -0
  15. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/README.md +0 -0
  16. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/django_pfx.egg-info/dependency_links.txt +0 -0
  17. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/django_pfx.egg-info/requires.txt +0 -0
  18. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/django_pfx.egg-info/top_level.txt +0 -0
  19. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/doc/Makefile +0 -0
  20. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/doc/conf.py +0 -0
  21. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/doc/index.rst +0 -0
  22. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/doc/source/api.views.rst +0 -0
  23. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/doc/source/authentication.md +0 -0
  24. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/doc/source/decorator.md +0 -0
  25. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/doc/source/generate_openapi.md +0 -0
  26. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/doc/source/getting_started.md +0 -0
  27. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/doc/source/internationalisation.md +0 -0
  28. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/doc/source/model.md +0 -0
  29. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/doc/source/pfx_views.md +0 -0
  30. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/doc/source/profiling.md +0 -0
  31. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/doc/source/settings.md +0 -0
  32. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/doc/source/testing.md +0 -0
  33. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/img/pfx.png +0 -0
  34. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/img/pfx.svg +0 -0
  35. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/make_messages +0 -0
  36. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/manage.py +0 -0
  37. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/__init__.py +0 -0
  38. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/__init__.py +0 -0
  39. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/apidoc/__init__.py +0 -0
  40. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/apidoc/parameters.py +0 -0
  41. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/apidoc/schema.py +0 -0
  42. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/apidoc/tags.py +0 -0
  43. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/apps.py +0 -0
  44. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/decorator/__init__.py +0 -0
  45. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/decorator/rest.py +0 -0
  46. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/default_settings.py +0 -0
  47. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/exceptions.py +0 -0
  48. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/fields/__init__.py +0 -0
  49. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/fields/decimal_field.py +0 -0
  50. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/fields/media_field.py +0 -0
  51. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/fields/minutes_duration_field.py +0 -0
  52. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/fields/nh3_field.py +0 -0
  53. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/http/__init__.py +0 -0
  54. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/http/json_response.py +0 -0
  55. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
  56. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/management/__init__.py +0 -0
  57. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/management/commands/__init__.py +0 -0
  58. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/management/commands/makeapidoc.py +0 -0
  59. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/management/commands/profile.py +0 -0
  60. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/middleware/__init__.py +0 -0
  61. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/middleware/authentication.py +0 -0
  62. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/middleware/locale.py +0 -0
  63. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/middleware/profiling.py +0 -0
  64. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/migrations/0001_initial.py +0 -0
  65. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/migrations/0002_pfxpermissionsuser.py +0 -0
  66. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/migrations/0003_delete_pfxpermissionsuser.py +0 -0
  67. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/migrations/__init__.py +0 -0
  68. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/migrations/operations/__init__.py +0 -0
  69. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/migrations/operations/permissions.py +0 -0
  70. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/models/__init__.py +0 -0
  71. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/models/abstract_pfx_base_user.py +0 -0
  72. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/models/cache_mixins.py +0 -0
  73. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/models/login_ban.py +0 -0
  74. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/models/not_null_fields.py +0 -0
  75. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/models/ordered_model_mixin.py +0 -0
  76. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/models/otp_user_mixin.py +0 -0
  77. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/models/pfx_user.py +0 -0
  78. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/models/user_filtered_queryset_mixin.py +0 -0
  79. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/serializers/__init__.py +0 -0
  80. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/serializers/json.py +0 -0
  81. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/settings.py +0 -0
  82. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/shortcuts.py +0 -0
  83. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/storage/__init__.py +0 -0
  84. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/storage/exceptions.py +0 -0
  85. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/storage/local_storage.py +0 -0
  86. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/storage/s3_storage.py +0 -0
  87. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/templates/registration/otp_code_email.txt +0 -0
  88. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/templates/registration/otp_code_subject.txt +0 -0
  89. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/templates/registration/password_reset_email.txt +0 -0
  90. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/templates/registration/password_reset_subject.txt +0 -0
  91. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/templates/registration/welcome_email.txt +0 -0
  92. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/templates/registration/welcome_subject.txt +0 -0
  93. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/test.py +0 -0
  94. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/urls.py +0 -0
  95. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/views/__init__.py +0 -0
  96. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/views/authentication_views.py +0 -0
  97. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/views/filters_views.py +0 -0
  98. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/views/locale_views.py +0 -0
  99. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/views/media_rest_view_mixin.py +0 -0
  100. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/views/ordered_rest_view_mixin.py +0 -0
  101. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/views/parameters/__init__.py +0 -0
  102. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/views/parameters/date_format.py +0 -0
  103. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/views/parameters/groups.py +0 -0
  104. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/views/parameters/list_count.py +0 -0
  105. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/views/parameters/list_items.py +0 -0
  106. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/views/parameters/list_mode.py +0 -0
  107. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/views/parameters/list_order.py +0 -0
  108. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/views/parameters/list_search.py +0 -0
  109. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/views/parameters/media_redirect.py +0 -0
  110. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/views/parameters/meta_fields.py +0 -0
  111. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/views/parameters/meta_filters.py +0 -0
  112. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/views/parameters/meta_orders.py +0 -0
  113. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/views/parameters/subset.py +0 -0
  114. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/views/parameters/subset_limit.py +0 -0
  115. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/views/parameters/subset_offset.py +0 -0
  116. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/views/parameters/subset_page.py +0 -0
  117. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/views/parameters/subset_page_size.py +0 -0
  118. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/pfxcore/views/parameters/subset_page_subset.py +0 -0
  119. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/settings/__init__.py +0 -0
  120. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pfx/settings/dev.py +0 -0
  121. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/pyproject.toml +0 -0
  122. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/requirements.txt +0 -0
  123. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/serve-doc +0 -0
  124. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/setup.cfg +0 -0
  125. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/setup.py +0 -0
  126. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/__init__.py +0 -0
  127. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/apps.py +0 -0
  128. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/locale/fr/LC_MESSAGES/django.po +0 -0
  129. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/migrations/0001_initial.py +0 -0
  130. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/migrations/0002_alter_book_cover.py +0 -0
  131. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/migrations/0003_book_local_file.py +0 -0
  132. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/migrations/__init__.py +0 -0
  133. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/models.py +0 -0
  134. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/settings/__init__.py +0 -0
  135. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/settings/ci.py +0 -0
  136. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/settings/common.py +0 -0
  137. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/settings/dev.py +0 -0
  138. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/settings/dev_custom_example.py +0 -0
  139. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/settings/dev_default.py +0 -0
  140. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/tests/basic_api_errors.py +0 -0
  141. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/tests/basic_api_test.py +0 -0
  142. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/tests/test_api_doc.py +0 -0
  143. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/tests/test_api_doc_search.py +0 -0
  144. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/tests/test_auth_api.py +0 -0
  145. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/tests/test_body_mixin.py +0 -0
  146. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/tests/test_cache.py +0 -0
  147. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/tests/test_client.py +0 -0
  148. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/tests/test_fields_choices.py +0 -0
  149. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/tests/test_fields_date.py +0 -0
  150. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/tests/test_fields_decimal.py +0 -0
  151. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/tests/test_fields_minutes_duration.py +0 -0
  152. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/tests/test_fields_nh3.py +0 -0
  153. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/tests/test_filters.py +0 -0
  154. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/tests/test_locale_api.py +0 -0
  155. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/tests/test_ordered_rest_view_mixin.py +0 -0
  156. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/tests/test_perm_tests.py +0 -0
  157. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/tests/test_permissions.py +0 -0
  158. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/tests/test_perms_api.py +0 -0
  159. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/tests/test_post_migrate_groups_update.py +0 -0
  160. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/tests/test_profiling_middleware.py +0 -0
  161. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/tests/test_settings.py +0 -0
  162. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/tests/test_shortcuts.py +0 -0
  163. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/tests/test_timezone_middleware.py +0 -0
  164. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/tests/test_tools.py +0 -0
  165. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/tests/test_user_queryset.py +0 -0
  166. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/tests/test_view_decorators.py +0 -0
  167. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/tests/test_view_fields.py +0 -0
  168. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/urls.py +0 -0
  169. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests/views.py +0 -0
  170. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests_base_user/__init__.py +0 -0
  171. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests_base_user/migrations/0001_initial.py +0 -0
  172. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests_base_user/migrations/__init__.py +0 -0
  173. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests_base_user/settings/__init__.py +0 -0
  174. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests_base_user/settings/ci.py +0 -0
  175. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests_base_user/settings/common.py +0 -0
  176. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests_base_user/settings/dev.py +0 -0
  177. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests_base_user/settings/dev_custom_example.py +0 -0
  178. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests_base_user/settings/dev_default.py +0 -0
  179. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests_base_user/tests/__init__.py +0 -0
  180. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests_base_user/tests/test_api.py +0 -0
  181. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests_base_user/tests/test_auth_api.py +0 -0
  182. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests_base_user/urls.py +0 -0
  183. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests_base_user/views.py +0 -0
  184. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests_custom_user/__init__.py +0 -0
  185. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests_custom_user/migrations/0001_initial.py +0 -0
  186. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests_custom_user/migrations/__init__.py +0 -0
  187. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests_custom_user/models.py +0 -0
  188. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests_custom_user/settings/__init__.py +0 -0
  189. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests_custom_user/settings/ci.py +0 -0
  190. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests_custom_user/settings/common.py +0 -0
  191. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests_custom_user/settings/dev.py +0 -0
  192. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests_custom_user/settings/dev_custom_example.py +0 -0
  193. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests_custom_user/settings/dev_default.py +0 -0
  194. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests_custom_user/tests/__init__.py +0 -0
  195. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests_custom_user/tests/test_api.py +0 -0
  196. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests_custom_user/tests/test_auth_api.py +0 -0
  197. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/tests_custom_user/urls.py +0 -0
  198. {django_pfx-1.4.dev144 → django_pfx-1.4.dev146}/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.dev146
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.dev146
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-11 10:12+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:1213
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):
@@ -97,8 +97,8 @@ class ViewField:
97
97
  def __init__(
98
98
  self, name, verbose_name=None, alias=None, field_type=None,
99
99
  readonly=False, readonly_create=False, readonly_update=False,
100
- choices=None, json_repr=None,
101
- related_model=None, related_model_api=None,
100
+ choices=None, json_repr=None, media_field_api=None,
101
+ related_model=None, related_model_api=None, related_fields=None,
102
102
  select_related=None, prefetch_related=None):
103
103
  self.name = name
104
104
  self.alias = alias or name
@@ -108,8 +108,13 @@ class ViewField:
108
108
  self.field_type = field_type
109
109
  self.choices = dict(choices or [])
110
110
  self.json_repr = json_repr
111
+ self.media_field_api = media_field_api
111
112
  self.related_model = related_model
112
113
  self.related_model_api = related_model_api
114
+ self.related_fields = None
115
+ if self.related_model and related_fields:
116
+ self.related_fields = process_fields(
117
+ self.related_model, related_fields)
113
118
  self.select_related = select_related or []
114
119
  self.prefetch_related = prefetch_related or []
115
120
  self.model_field = None
@@ -126,6 +131,9 @@ class ViewField:
126
131
  res['model'] = self.related_model._meta.label
127
132
  res['api'] = self.related_model_api or getattr(
128
133
  self.related_model, 'api', None)
134
+ if self.related_fields:
135
+ res['fields'] = {
136
+ n: f.meta() for n, f in self.related_fields.items()}
129
137
  res['readonly'] = dict(
130
138
  post=self.readonly_create,
131
139
  put=self.readonly_update)
@@ -144,27 +152,39 @@ class ViewField:
144
152
  return None
145
153
  return getattr(obj, name)
146
154
 
147
- def _json_repr(self, value):
155
+ def _related_json_repr(self, value):
148
156
  if not value:
149
157
  return None
150
158
  if self.json_repr:
151
- return self.json_repr(value)
152
- return value.json_repr()
159
+ vals = self.json_repr(value)
160
+ else:
161
+ vals = value.json_repr()
162
+ if self.related_fields:
163
+ vals.update({
164
+ n: f.to_json(value) for n, f in self.related_fields.items()})
165
+ return vals
153
166
 
154
- def to_json(self, view, obj):
167
+ def to_json(self, obj, view=None):
155
168
  value = self.get_value(obj)
156
169
 
157
170
  if self.field_type == FieldType.ModelObject:
158
- return self._json_repr(value)
171
+ return self._related_json_repr(value)
159
172
  if self.field_type == FieldType.ModelObjectList:
160
- return [self._json_repr(o) for o in value.all()]
173
+ return [self._related_json_repr(o) for o in value.all()]
161
174
 
162
175
  if self.json_repr:
163
176
  return self.json_repr(value)
164
177
 
165
178
  if self.field_type == FieldType.MediaField:
166
179
  if value:
167
- api_url = view._rest_view_path
180
+ if self.media_field_api:
181
+ api_url = self.media_field_api
182
+ elif view:
183
+ api_url = view._rest_view_path
184
+ else:
185
+ raise Exception(
186
+ "media_field_api must be defined if the field "
187
+ "is not a view root field.")
168
188
  value['url'] = f'{api_url}/{obj.pk}/{self.name}'
169
189
  return value
170
190
  else:
@@ -317,6 +337,7 @@ class ViewField:
317
337
  class ViewModelField(ViewField):
318
338
  def __init__(
319
339
  self, name, model_field=None, **kwargs):
340
+ related_fields = kwargs.get('related_fields')
320
341
  super().__init__(name, **kwargs)
321
342
  self.model_field = model_field
322
343
  if hasattr(model_field, 'to_json'):
@@ -324,6 +345,9 @@ class ViewModelField(ViewField):
324
345
  if (hasattr(self.model_field, 'related_model') and
325
346
  self.model_field.related_model and not self.related_model):
326
347
  self.related_model = self.model_field.related_model
348
+ if self.related_model and related_fields:
349
+ self.related_fields = process_fields(
350
+ self.related_model, related_fields)
327
351
  if not self.select_related and (
328
352
  self.field_type == FieldType.ModelObject):
329
353
  # Auto add the field in select_related if select_related
@@ -350,7 +374,13 @@ class ViewModelField(ViewField):
350
374
 
351
375
  def to_model_value(self, value, get_related_queryset):
352
376
  def _get_obj(v):
353
- pk = v['pk'] if isinstance(v, dict) and 'pk' in v else v
377
+ if isinstance(v, dict):
378
+ if 'pk' in v:
379
+ pk = v['pk']
380
+ else:
381
+ return self.model_field.related_model()
382
+ else:
383
+ pk = v
354
384
  return pk and get_object(
355
385
  get_related_queryset(self.model_field.related_model),
356
386
  related_field=self.name, pk=pk) or None
@@ -397,7 +427,7 @@ class VF:
397
427
  readonly=False, readonly_create=False, readonly_update=False,
398
428
  choices=None, select_related=None, prefetch_related=None,
399
429
  json_repr=None, related_model=None, related_model_api=None,
400
- field=None):
430
+ field=None, related_fields=None, media_field_api=None):
401
431
  self.kwargs = dict(
402
432
  name=name, verbose_name=verbose_name, field_type=field_type,
403
433
  alias=alias,
@@ -405,7 +435,26 @@ class VF:
405
435
  readonly_update=readonly_update, choices=choices,
406
436
  select_related=select_related, prefetch_related=prefetch_related,
407
437
  json_repr=json_repr, related_model=related_model,
408
- related_model_api=related_model_api, field=field)
438
+ related_model_api=related_model_api, field=field,
439
+ related_fields=related_fields, media_field_api=media_field_api)
409
440
 
410
441
  def to_field(self, model):
411
442
  return ViewField.from_name(model, **self.kwargs)
443
+
444
+
445
+ def process_fields(model, fields):
446
+ if not fields:
447
+ return {
448
+ _f.name: ViewField.from_model_field(_f.name, _f)
449
+ for _f in model._meta.fields}
450
+
451
+ def _field(e):
452
+ if isinstance(e, ViewField):
453
+ field = e
454
+ elif isinstance(e, VF):
455
+ field = e.to_field(model)
456
+ else:
457
+ field = ViewField.from_name(model, e)
458
+ return field.alias, field
459
+
460
+ return dict(_field(e) for e in fields)
@@ -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,12 @@ 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
+ for o in v:
374
+ save_field = getattr(o, '_save_related', False)
375
+ if save_field:
376
+ setattr(o, save_field, obj)
377
+ o.save()
359
378
  getattr(obj, k).set(v)
360
379
  self.post_save(obj, created=created, funcs=funcs)
361
380
  obj = self.get_object(pk=obj.pk)
@@ -499,7 +518,7 @@ class ModelBodyMixin(BodyMixin, ModelMixin):
499
518
  """
500
519
  fields = self.get_fields()
501
520
 
502
- def can_write(fname):
521
+ def can_write(fname, fields):
503
522
  if fname not in fields:
504
523
  return False
505
524
  if fields[fname].is_readonly(created=created):
@@ -512,9 +531,19 @@ class ModelBodyMixin(BodyMixin, ModelMixin):
512
531
  res = {}
513
532
  res_rel = {}
514
533
  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:
534
+ if can_write(k, fields):
535
+ field = fields[k]
536
+ mk, mv = field.to_model_value(v, self.get_related_queryset)
537
+ if field.field_type == FT.ModelObjectList:
538
+ if field.related_fields:
539
+ for i, rv in enumerate(v):
540
+ for rk, rf in field.related_fields.items():
541
+ rmk, rmv = rf.to_model_value(
542
+ rv.get(rk), self.get_related_queryset)
543
+ setattr(mv[i], rmk, rmv)
544
+ setattr(
545
+ mv[i], '_save_related',
546
+ field.model_field.field.name)
518
547
  res_rel[mk] = mv
519
548
  else:
520
549
  res[mk] = mv
@@ -768,7 +797,7 @@ class ListRestViewMixin(ModelResponseMixin):
768
797
  *self.get_list_fields_prefetch_related())
769
798
  for o in qs:
770
799
  yield self.serialize_object(o, **{
771
- _f.alias: _f.to_json(self, o)
800
+ _f.alias: _f.to_json(o, self)
772
801
  for _f in self.get_list_fields().values()})
773
802
 
774
803
  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,178 @@
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."])
File without changes