django-pfx 1.4.dev54__tar.gz → 1.4.dev58__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 (143) hide show
  1. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/PKG-INFO +1 -1
  2. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/django_pfx.egg-info/PKG-INFO +1 -1
  3. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.po +4 -4
  4. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/models/pfx_models.py +1 -0
  5. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/views/filters_views.py +25 -5
  6. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/views/rest_views.py +36 -20
  7. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/models.py +10 -1
  8. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/tests/basic_api_errors.py +4 -3
  9. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/tests/basic_api_test.py +37 -0
  10. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/views.py +1 -6
  11. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/.gitignore +0 -0
  12. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/.gitlab-ci.yml +0 -0
  13. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/.pre-commit-config.yaml +0 -0
  14. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/LICENSE +0 -0
  15. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/MANIFEST.in +0 -0
  16. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/README.md +0 -0
  17. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/django_pfx.egg-info/SOURCES.txt +0 -0
  18. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/django_pfx.egg-info/dependency_links.txt +0 -0
  19. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/django_pfx.egg-info/requires.txt +0 -0
  20. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/django_pfx.egg-info/top_level.txt +0 -0
  21. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/doc/Makefile +0 -0
  22. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/doc/conf.py +0 -0
  23. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/doc/index.rst +0 -0
  24. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/doc/source/api.views.rst +0 -0
  25. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/doc/source/authentication.md +0 -0
  26. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/doc/source/decorator.md +0 -0
  27. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/doc/source/generate_openapi.md +0 -0
  28. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/doc/source/getting_started.md +0 -0
  29. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/doc/source/internationalisation.md +0 -0
  30. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/doc/source/model.md +0 -0
  31. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/doc/source/pfx_views.md +0 -0
  32. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/doc/source/profiling.md +0 -0
  33. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/doc/source/settings.md +0 -0
  34. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/doc/source/testing.md +0 -0
  35. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/img/pfx.png +0 -0
  36. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/img/pfx.svg +0 -0
  37. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/make_messages +0 -0
  38. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/manage.py +0 -0
  39. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/__init__.py +0 -0
  40. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/__init__.py +0 -0
  41. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/apidoc/__init__.py +0 -0
  42. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/apidoc/parameters.py +0 -0
  43. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/apidoc/schema.py +0 -0
  44. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/apidoc/tags.py +0 -0
  45. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/apps.py +0 -0
  46. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/decorator/__init__.py +0 -0
  47. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/decorator/rest.py +0 -0
  48. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/default_settings.py +0 -0
  49. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/exceptions.py +0 -0
  50. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/fields.py +0 -0
  51. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/http/__init__.py +0 -0
  52. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/http/json_response.py +0 -0
  53. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
  54. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/management/__init__.py +0 -0
  55. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/management/commands/__init__.py +0 -0
  56. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/management/commands/makeapidoc.py +0 -0
  57. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/management/commands/profile.py +0 -0
  58. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/middleware/__init__.py +0 -0
  59. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/middleware/authentication.py +0 -0
  60. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/middleware/locale.py +0 -0
  61. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/middleware/profiling.py +0 -0
  62. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/migrations/0001_initial.py +0 -0
  63. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/migrations/__init__.py +0 -0
  64. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/models/__init__.py +0 -0
  65. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/models/abstract_pfx_base_user.py +0 -0
  66. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/models/cache_mixins.py +0 -0
  67. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/models/login_ban.py +0 -0
  68. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/models/not_null_fields.py +0 -0
  69. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/models/otp_user_mixin.py +0 -0
  70. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/models/pfx_user.py +0 -0
  71. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/models/user_filtered_queryset_mixin.py +0 -0
  72. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/serializers/__init__.py +0 -0
  73. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/serializers/json.py +0 -0
  74. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/settings.py +0 -0
  75. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/shortcuts.py +0 -0
  76. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/storage/__init__.py +0 -0
  77. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/storage/s3_storage.py +0 -0
  78. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/templates/registration/otp_code_email.txt +0 -0
  79. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/templates/registration/otp_code_subject.txt +0 -0
  80. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/templates/registration/password_reset_email.txt +0 -0
  81. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/templates/registration/password_reset_subject.txt +0 -0
  82. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/templates/registration/welcome_email.txt +0 -0
  83. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/templates/registration/welcome_subject.txt +0 -0
  84. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/test.py +0 -0
  85. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/urls.py +0 -0
  86. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/views/__init__.py +0 -0
  87. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/views/authentication_views.py +0 -0
  88. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/views/fields.py +0 -0
  89. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/views/locale_views.py +0 -0
  90. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/views/parameters/__init__.py +0 -0
  91. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/views/parameters/date_format.py +0 -0
  92. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/views/parameters/groups.py +0 -0
  93. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/views/parameters/list_count.py +0 -0
  94. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/views/parameters/list_items.py +0 -0
  95. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/views/parameters/list_mode.py +0 -0
  96. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/views/parameters/list_order.py +0 -0
  97. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/views/parameters/list_search.py +0 -0
  98. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/views/parameters/media_redirect.py +0 -0
  99. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/views/parameters/meta_fields.py +0 -0
  100. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/views/parameters/meta_filters.py +0 -0
  101. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/views/parameters/meta_orders.py +0 -0
  102. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/views/parameters/subset.py +0 -0
  103. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/views/parameters/subset_limit.py +0 -0
  104. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/views/parameters/subset_offset.py +0 -0
  105. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/views/parameters/subset_page.py +0 -0
  106. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/views/parameters/subset_page_size.py +0 -0
  107. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/pfxcore/views/parameters/subset_page_subset.py +0 -0
  108. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/settings/__init__.py +0 -0
  109. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pfx/settings/dev.py +0 -0
  110. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/pyproject.toml +0 -0
  111. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/requirements.txt +0 -0
  112. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/serve-doc +0 -0
  113. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/setup.cfg +0 -0
  114. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/setup.py +0 -0
  115. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/__init__.py +0 -0
  116. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/locale/fr/LC_MESSAGES/django.po +0 -0
  117. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/settings/__init__.py +0 -0
  118. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/settings/ci.py +0 -0
  119. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/settings/common.py +0 -0
  120. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/settings/dev.py +0 -0
  121. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/settings/dev_custom_example.py +0 -0
  122. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/settings/dev_default.py +0 -0
  123. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/tests/__init__.py +0 -0
  124. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/tests/test_api_doc.py +0 -0
  125. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/tests/test_api_doc_search.py +0 -0
  126. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/tests/test_auth_api.py +0 -0
  127. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/tests/test_body_mixin.py +0 -0
  128. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/tests/test_cache.py +0 -0
  129. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/tests/test_client.py +0 -0
  130. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/tests/test_fields.py +0 -0
  131. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/tests/test_filters.py +0 -0
  132. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/tests/test_locale_api.py +0 -0
  133. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/tests/test_perm_tests.py +0 -0
  134. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/tests/test_perms_api.py +0 -0
  135. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/tests/test_profiling_middleware.py +0 -0
  136. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/tests/test_settings.py +0 -0
  137. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/tests/test_shortcuts.py +0 -0
  138. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/tests/test_timezone_middleware.py +0 -0
  139. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/tests/test_tools.py +0 -0
  140. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/tests/test_user_queryset.py +0 -0
  141. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/tests/test_view_decorators.py +0 -0
  142. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/tests/test_view_fields.py +0 -0
  143. {django_pfx-1.4.dev54 → django_pfx-1.4.dev58}/tests/urls.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: django-pfx
3
- Version: 1.4.dev54
3
+ Version: 1.4.dev58
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.1
2
2
  Name: django-pfx
3
- Version: 1.4.dev54
3
+ Version: 1.4.dev58
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
@@ -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: 2024-08-20 12:22+0200\n"
10
+ "POT-Creation-Date: 2024-12-11 12:51+0100\n"
11
11
  "PO-Revision-Date: 2021-06-22 23:31+0200\n"
12
12
  "Last-Translator: \n"
13
13
  "Language-Team: \n"
@@ -254,7 +254,7 @@ msgstr ""
254
254
  msgid "A new authentication code has been sent by email."
255
255
  msgstr "Un nouveau code d'authentification a été envoyé par e-mail."
256
256
 
257
- #: views/filters_views.py:79
257
+ #: views/filters_views.py:81
258
258
  #, python-brace-format
259
259
  msgid "Invalid value for {filter} filter"
260
260
  msgstr "Valeur invalide pour le filtre {filter}"
@@ -275,11 +275,11 @@ msgstr "{model} {obj} créé."
275
275
  msgid "{model} {obj} updated."
276
276
  msgstr "{model} {obj} modifié."
277
277
 
278
- #: views/rest_views.py:1077
278
+ #: views/rest_views.py:1093
279
279
  #, python-brace-format
280
280
  msgid "{model} {obj} deleted."
281
281
  msgstr "{model} {obj} supprimé."
282
282
 
283
- #: views/rest_views.py:1169 views/rest_views.py:1209
283
+ #: views/rest_views.py:1185 views/rest_views.py:1225
284
284
  msgid "Unexpected storage error"
285
285
  msgstr "Erreur de stockage inattendue"
@@ -22,6 +22,7 @@ class UniqueConstraint(models.UniqueConstraint):
22
22
 
23
23
  class JSONReprMixin():
24
24
  """A model mixin to manage JSON representation."""
25
+ api = None
25
26
  apidoc = {}
26
27
 
27
28
  def json_repr(self, **values):
@@ -34,8 +34,8 @@ class Filter():
34
34
  def __init__(
35
35
  self, name, label, type=None, filter_func=None,
36
36
  filter_func_and=False, filter_func_list=False, choices=None,
37
- related_model=None, technical=False, defaults=None,
38
- empty_value=True):
37
+ related_model=None, related_model_api=None,
38
+ technical=False, defaults=None, empty_value=True):
39
39
  self.name = name
40
40
  self.label = label
41
41
  self.type = type
@@ -44,6 +44,7 @@ class Filter():
44
44
  self.filter_func_list = filter_func_list
45
45
  self.choices = choices
46
46
  self.related_model = related_model
47
+ self.related_model_api = related_model_api
47
48
  self.technical = technical
48
49
  self.defaults = defaults or []
49
50
  self.empty_value = empty_value
@@ -62,6 +63,7 @@ class Filter():
62
63
  dict(label=_(v), value=k) for k, v in self.choices]
63
64
  if self.related_model:
64
65
  res['related_model'] = str(self.related_model.__name__)
66
+ res['api'] = self.related_model_api
65
67
  return res
66
68
 
67
69
  def _parse_value(self, value):
@@ -87,7 +89,20 @@ class Filter():
87
89
  [self.filter_func(v) for v in values])
88
90
 
89
91
  def _get_values(self, params):
90
- values = [self._parse_value(v) for v in params.getlist(self.name)]
92
+ values = []
93
+ if (self.type == FieldType.ModelObject and self.related_model and
94
+ hasattr(self.related_model.objects, 'default_search')):
95
+ q = None
96
+ for search in params.getlist(f'{self.name}*'):
97
+ crit = self.related_model.objects.default_search(search)
98
+ if q is None:
99
+ q = crit
100
+ q |= crit
101
+ if q:
102
+ values.extend(self.related_model.objects.filter(
103
+ q).values_list('pk', flat=True))
104
+ values.extend([
105
+ self._parse_value(v) for v in params.getlist(self.name)])
91
106
  return values or self.defaults
92
107
 
93
108
  def query(self, params):
@@ -99,8 +114,8 @@ class ModelFilter(Filter):
99
114
  def __init__(
100
115
  self, model, name, label=None, type=None,
101
116
  filter_func=None, filter_func_and=False, filter_func_list=False,
102
- choices=None, related_model=None, technical=False, defaults=None,
103
- empty_value=None):
117
+ choices=None, related_model=None, related_model_api=None,
118
+ technical=False, defaults=None, empty_value=None):
104
119
  self.model = model
105
120
  self.field = model._meta.get_field(name)
106
121
  if empty_value is None:
@@ -113,6 +128,11 @@ class ModelFilter(Filter):
113
128
  related_model or (
114
129
  self.field.remote_field and
115
130
  self.field.remote_field.model),
131
+ related_model_api or (
132
+ self.field.remote_field and
133
+ self.field.remote_field.model and
134
+ hasattr(self.field.remote_field.model, 'api') and
135
+ self.field.remote_field.model.api),
116
136
  technical=technical, defaults=defaults, empty_value=empty_value)
117
137
 
118
138
  @property
@@ -143,8 +143,8 @@ class ModelMixin():
143
143
  def _process_fields(cls, fields):
144
144
  if not fields:
145
145
  return {
146
- f.name: ViewField.from_model_field(f.name, f)
147
- for f in cls.model._meta.fields}
146
+ _f.name: ViewField.from_model_field(_f.name, _f)
147
+ for _f in cls.model._meta.fields}
148
148
 
149
149
  def _field(e):
150
150
  if isinstance(e, ViewField):
@@ -178,8 +178,8 @@ class ModelMixin():
178
178
  return cache.get_or_set(
179
179
  class_key(cls, 'fields', 'select_related'),
180
180
  lambda: set([
181
- f for field in cls.get_fields().values()
182
- for f in field.select_related]),
181
+ _f for field in cls.get_fields().values()
182
+ for _f in field.select_related]),
183
183
  None)
184
184
 
185
185
  @classmethod
@@ -188,8 +188,8 @@ class ModelMixin():
188
188
  return cache.get_or_set(
189
189
  class_key(cls, 'fields', 'prefetch_related'),
190
190
  lambda: set([
191
- f for field in cls.get_fields().values()
192
- for f in field.prefetch_related]),
191
+ _f for field in cls.get_fields().values()
192
+ for _f in field.prefetch_related]),
193
193
  None)
194
194
 
195
195
  @property
@@ -286,8 +286,8 @@ class ModelResponseMixin(ModelMixin):
286
286
  :rtype: :class:`JsonResponse`
287
287
  """
288
288
  return JsonResponse(self.serialize_object(o, **{
289
- f.alias: f.to_json(o, self.format_date)
290
- for f in self.get_fields().values()}, meta=meta))
289
+ _f.alias: _f.to_json(o, self.format_date)
290
+ for _f in self.get_fields().values()}, meta=meta))
291
291
 
292
292
  def validate(self, obj, created=False, **kwargs):
293
293
  """Validate an object instance.
@@ -434,8 +434,8 @@ class BodyMixin:
434
434
  """
435
435
  if fields is None:
436
436
  fields = [
437
- f.name for f in model._meta.get_fields()
438
- if not isinstance(f, AutoFieldMixin)]
437
+ _f.name for _f in model._meta.get_fields()
438
+ if not isinstance(_f, AutoFieldMixin)]
439
439
  obj = model(**{
440
440
  k: v for k, v in self.deserialize_body().items() if k in fields})
441
441
  if validate:
@@ -531,6 +531,15 @@ class ListRestViewMixin(ModelResponseMixin):
531
531
  self.list_fields or self.fields)), None))
532
532
  return self._list_fields
533
533
 
534
+ def get_list_meta_filters(self):
535
+ """Return the filters metadata for lists.
536
+
537
+ :returns: The filters metadata generator
538
+ :rtype: :class:`generator`
539
+ """
540
+ for _f in self.filters:
541
+ yield _f.meta
542
+
534
543
  def search_filter(self, search): # pragma: no cover
535
544
  """Return the django filters for the default text search.
536
545
 
@@ -541,6 +550,8 @@ class ListRestViewMixin(ModelResponseMixin):
541
550
  :returns: The django filters
542
551
  :rtype: :class:`django.db.models.Q`
543
552
  """
553
+ if hasattr(self.model.objects, 'default_search'):
554
+ return self.model.objects.default_search(search)
544
555
  return Q()
545
556
 
546
557
  def orderable_fields(self, model, models=None):
@@ -577,7 +588,7 @@ class ListRestViewMixin(ModelResponseMixin):
577
588
  if get_bool(self.request.GET, 'filters', default_all):
578
589
  meta['filters'] = cache.get_or_set(
579
590
  class_key(self.__class__, 'meta', 'filters'),
580
- lambda: [f.meta for f in self.filters],
591
+ lambda: [_f.meta for _f in self.filters],
581
592
  None)
582
593
  if get_bool(self.request.GET, 'orders', default_all):
583
594
  meta['orders'] = cache.get_or_set(
@@ -620,9 +631,14 @@ class ListRestViewMixin(ModelResponseMixin):
620
631
  :returns: The filtered queryset
621
632
  :rtype: :class:`django.db.models.QuerySet`
622
633
  """
623
- search = self.request.GET.get('search')
624
- if search:
625
- return qs.filter(self.search_filter(search))
634
+ q = None
635
+ for search in self.request.GET.getlist('search'):
636
+ crit = self.search_filter(search)
637
+ if q is None:
638
+ q = crit
639
+ q |= crit
640
+ if q:
641
+ return qs.filter(q)
626
642
  return qs
627
643
 
628
644
  def get_order_mapping(self):
@@ -681,8 +697,8 @@ class ListRestViewMixin(ModelResponseMixin):
681
697
  return cache.get_or_set(
682
698
  class_key(self.__class__, 'list_fields', 'select_related'),
683
699
  lambda: set([
684
- f for field in self.get_list_fields().values()
685
- for f in field.select_related]),
700
+ _f for field in self.get_list_fields().values()
701
+ for _f in field.select_related]),
686
702
  None)
687
703
 
688
704
  def get_list_fields_prefetch_related(self):
@@ -690,8 +706,8 @@ class ListRestViewMixin(ModelResponseMixin):
690
706
  return cache.get_or_set(
691
707
  class_key(self.__class__, 'list_fields', 'prefetch_related'),
692
708
  lambda: set([
693
- f for field in self.get_list_fields().values()
694
- for f in field.prefetch_related]),
709
+ _f for field in self.get_list_fields().values()
710
+ for _f in field.prefetch_related]),
695
711
  None)
696
712
 
697
713
  def get_list_result(self, qs):
@@ -708,8 +724,8 @@ class ListRestViewMixin(ModelResponseMixin):
708
724
  *self.get_list_fields_prefetch_related())
709
725
  for o in qs:
710
726
  yield self.serialize_object(o, **{
711
- f.alias: f.to_json(o, self.format_date)
712
- for f in self.get_list_fields().values()})
727
+ _f.alias: _f.to_json(o, self.format_date)
728
+ for _f in self.get_list_fields().values()})
713
729
 
714
730
  def get_short_list_result(self, qs):
715
731
  """Get a generator to serialize each result in a queryset.
@@ -1,6 +1,7 @@
1
1
  from django.contrib.auth.base_user import BaseUserManager
2
2
  from django.core.mail import send_mail
3
3
  from django.db import models
4
+ from django.db.models import Q
4
5
  from django.utils.functional import cached_property
5
6
  from django.utils.translation import gettext_lazy as _
6
7
 
@@ -96,6 +97,13 @@ class User(CacheableMixin, JSONReprMixin, OtpUserMixin, AbstractPFXBaseUser):
96
97
  send_mail(subject, message, from_email, [self.email], **kwargs)
97
98
 
98
99
 
100
+ class AuthorQuerySet(models.QuerySet):
101
+ def default_search(self, search):
102
+ return (
103
+ Q(first_name__unaccent__icontains=search) |
104
+ Q(last_name__unaccent__icontains=search))
105
+
106
+
99
107
  class BadUserAuthorQuerySet(UserFilteredQuerySetMixin, models.QuerySet):
100
108
  pass
101
109
 
@@ -109,6 +117,7 @@ class UserAuthorQuerySet(UserFilteredQuerySetMixin, models.QuerySet):
109
117
 
110
118
  class Author(CacheableMixin, JSONReprMixin, models.Model):
111
119
  CACHED_PROPERTIES = ['books_count']
120
+ api = '/authors'
112
121
 
113
122
  first_name = models.CharField(_("First Name"), max_length=30)
114
123
  last_name = models.CharField(_("Last Name"), max_length=30)
@@ -126,7 +135,7 @@ class Author(CacheableMixin, JSONReprMixin, models.Model):
126
135
  'tests.BookType', related_name='authors',
127
136
  verbose_name="Types")
128
137
 
129
- objects = models.QuerySet.as_manager()
138
+ objects = AuthorQuerySet.as_manager()
130
139
  bad_user_objects = BadUserAuthorQuerySet.as_manager()
131
140
  user_objects = UserAuthorQuerySet.as_manager()
132
141
 
@@ -59,7 +59,8 @@ class BasicAPIErrorTest(TestAssertMixin, TestCase):
59
59
  "new_password": "Wrong stuffs",
60
60
  }''')
61
61
  self.assertRC(response, 422)
62
- self.assertJE(
63
- response, 'message',
62
+ self.assertIn(self.get_val(response, 'message'), [
63
+ "JSON Malformed Illegal trailing comma before end "
64
+ "of object: line 3 column 47 (char 96)", # python >= 3.13
64
65
  "JSON Malformed Expecting property name enclosed in "
65
- "double quotes: line 4 column 17 (char 114)")
66
+ "double quotes: line 4 column 17 (char 114)"]) # python <= 3.12
@@ -197,6 +197,12 @@ class BasicAPITest(TestAssertMixin, TestCase):
197
197
  self.assertJE(response, 'items.@0.gender.value', 'male')
198
198
  self.assertJE(response, 'items.@0.gender.label', 'Male')
199
199
 
200
+ response = self.client.get(
201
+ '/api/authors?search=isaac&search=tolkien&items=1&count=1')
202
+
203
+ self.assertRC(response, 200)
204
+ self.assertJE(response, 'meta.count', 2)
205
+
200
206
  def test_meta_list(self):
201
207
  response = self.client.get('/api/authors/meta/list')
202
208
  self.assertRC(response, 200)
@@ -224,6 +230,7 @@ class BasicAPITest(TestAssertMixin, TestCase):
224
230
  "label": "Types",
225
231
  "name": "types",
226
232
  "related_model": "BookType",
233
+ "api": "/book-types",
227
234
  "technical": False,
228
235
  "type": "ModelObject"
229
236
  }
@@ -286,6 +293,18 @@ class BasicAPITest(TestAssertMixin, TestCase):
286
293
 
287
294
  response = self.client.get('/api/books/meta/list')
288
295
  self.assertRC(response, 200)
296
+
297
+ self.assertJE(response, 'filters.@0.items.@0', {
298
+ "empty_value": False,
299
+ "is_group": False,
300
+ "label": "Author",
301
+ "name": "author",
302
+ "related_model": "Author",
303
+ "api": "/authors",
304
+ "technical": False,
305
+ "type": "ModelObject"
306
+ })
307
+
289
308
  self.assertJIn(response, 'orders', 'pk')
290
309
  self.assertJIn(response, 'orders', 'name')
291
310
  self.assertJIn(response, 'orders', 'author')
@@ -470,6 +489,24 @@ class BasicAPITest(TestAssertMixin, TestCase):
470
489
  self.assertTrue(
471
490
  item['type'] is None or item['type']['pk'] == book_type.pk)
472
491
 
492
+ def test_model_filter_foreign_key_text_search(self):
493
+ response = self.client.get(
494
+ '/api/books?author*=asimov&items=1&count=1')
495
+ self.assertRC(response, 200)
496
+ self.assertJE(response, 'meta.count', 3)
497
+
498
+ # Test multiple values
499
+ response = self.client.get(
500
+ '/api/books?author*=asimov&author*=tolkien&items=1&count=1')
501
+ self.assertRC(response, 200)
502
+ self.assertJE(response, 'meta.count', 7)
503
+
504
+ # Test empty search
505
+ response = self.client.get(
506
+ '/api/books?author*=&items=1&count=1')
507
+ self.assertRC(response, 200)
508
+ self.assertJE(response, 'meta.count', 7)
509
+
473
510
  def test_model_filter_char_choices(self):
474
511
  response = self.client.get('/api/authors?gender=male&items=1&count=1')
475
512
 
@@ -64,7 +64,7 @@ class AuthorRestView(AuthorRestViewMixin, SlugDetailRestViewMixin, RestView):
64
64
  Filter(
65
65
  'heroic_fantasy', _("Heroic Fantasy"),
66
66
  FieldType.BooleanField, heroic_fantasy_filter),
67
- ModelFilter(Author, 'types')
67
+ ModelFilter(Author, 'types', related_model_api='/book-types')
68
68
  ]),
69
69
  FilterGroup('custom', _("Custom"), [
70
70
  ModelFilter(
@@ -80,11 +80,6 @@ class AuthorRestView(AuthorRestViewMixin, SlugDetailRestViewMixin, RestView):
80
80
  ]),
81
81
  ]
82
82
 
83
- def search_filter(self, search):
84
- return (
85
- Q(first_name__unaccent__icontains=search) |
86
- Q(last_name__unaccent__icontains=search))
87
-
88
83
  @rest_api("/cache/<int:id>", method="get", groups=['cache'])
89
84
  def cache_get(self, id, *args, **kwargs):
90
85
  book = Author.cache_get(id)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes