wbcore 1.46.0__py2.py3-none-any.whl → 1.58.2__py2.py3-none-any.whl

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 (321) hide show
  1. wbcore/cache/decorators.py +5 -3
  2. wbcore/cache/registry.py +14 -7
  3. wbcore/configs/__init__.py +1 -0
  4. wbcore/configs/configs.py +5 -0
  5. wbcore/configs/decorators.py +1 -1
  6. wbcore/configurations/configurations/apps.py +3 -2
  7. wbcore/configurations/configurations/authentication.py +1 -1
  8. wbcore/configurations/configurations/base.py +1 -1
  9. wbcore/configurations/configurations/cache.py +1 -1
  10. wbcore/configurations/configurations/i18nl10n.py +2 -1
  11. wbcore/configurations/configurations/maintenance.py +1 -1
  12. wbcore/configurations/configurations/media.py +1 -1
  13. wbcore/configurations/configurations/middleware.py +1 -1
  14. wbcore/configurations/configurations/rest_framework.py +1 -1
  15. wbcore/configurations/configurations/static.py +3 -3
  16. wbcore/configurations/configurations/wbcore.py +1 -1
  17. wbcore/content_type/serializers.py +13 -5
  18. wbcore/content_type/utils.py +3 -3
  19. wbcore/content_type/viewsets.py +2 -2
  20. wbcore/contrib/agenda/filters/calendar_item.py +5 -4
  21. wbcore/contrib/agenda/locale/de/LC_MESSAGES/django.po +145 -52
  22. wbcore/contrib/agenda/locale/de/LC_MESSAGES/django.po.translated +236 -0
  23. wbcore/contrib/agenda/locale/en/LC_MESSAGES/django.po +200 -0
  24. wbcore/contrib/agenda/locale/fr/LC_MESSAGES/django.po +201 -0
  25. wbcore/contrib/agenda/viewsets/calendar_items.py +7 -7
  26. wbcore/contrib/agenda/viewsets/menu/calendar_items.py +0 -6
  27. wbcore/contrib/ai/exceptions.py +5 -5
  28. wbcore/contrib/ai/llm/config.py +76 -27
  29. wbcore/contrib/ai/llm/mixins.py +5 -8
  30. wbcore/contrib/ai/llm/utils.py +50 -26
  31. wbcore/contrib/authentication/admin.py +2 -2
  32. wbcore/contrib/authentication/factories/__init__.py +8 -1
  33. wbcore/contrib/authentication/factories/users.py +19 -0
  34. wbcore/contrib/authentication/filters.py +1 -2
  35. wbcore/contrib/authentication/locale/de/LC_MESSAGES/django.po +209 -187
  36. wbcore/contrib/authentication/locale/de/LC_MESSAGES/django.po.translated +634 -0
  37. wbcore/contrib/authentication/locale/en/LC_MESSAGES/django.po +590 -0
  38. wbcore/contrib/authentication/locale/fr/LC_MESSAGES/django.po +592 -0
  39. wbcore/contrib/authentication/models/users.py +3 -3
  40. wbcore/contrib/authentication/models/users_activities.py +1 -1
  41. wbcore/contrib/authentication/serializers/users.py +2 -2
  42. wbcore/contrib/authentication/tests/test_tokens.py +3 -3
  43. wbcore/contrib/authentication/tests/test_users.py +0 -1
  44. wbcore/contrib/authentication/urls.py +0 -4
  45. wbcore/contrib/authentication/viewsets/endpoints/user_activities.py +2 -11
  46. wbcore/contrib/authentication/viewsets/endpoints/users.py +0 -3
  47. wbcore/contrib/authentication/viewsets/user_activities.py +2 -1
  48. wbcore/contrib/authentication/viewsets/users.py +6 -4
  49. wbcore/contrib/color/models.py +2 -1
  50. wbcore/contrib/currency/factories.py +1 -1
  51. wbcore/contrib/currency/import_export/backends/fixerio/currency_fx_rates.py +3 -1
  52. wbcore/contrib/currency/models.py +30 -8
  53. wbcore/contrib/currency/serializers.py +5 -1
  54. wbcore/contrib/currency/tests/test_serializers.py +7 -3
  55. wbcore/contrib/currency/tests/test_viewsets.py +1 -1
  56. wbcore/contrib/currency/viewsets/currency.py +2 -2
  57. wbcore/contrib/currency/viewsets/endpoints/currency_fx_rates.py +0 -9
  58. wbcore/contrib/dataloader/tests/test/dataloaders/protocols.py +1 -2
  59. wbcore/contrib/dataloader/utils.py +2 -2
  60. wbcore/contrib/directory/factories/__init__.py +1 -1
  61. wbcore/contrib/directory/factories/entries.py +2 -1
  62. wbcore/contrib/directory/filters/entries.py +9 -0
  63. wbcore/contrib/directory/locale/de/LC_MESSAGES/django.po +728 -714
  64. wbcore/contrib/directory/locale/de/LC_MESSAGES/django.po.translated +1779 -0
  65. wbcore/contrib/directory/locale/en/LC_MESSAGES/django.po +1652 -0
  66. wbcore/contrib/directory/locale/fr/LC_MESSAGES/django.po +1654 -0
  67. wbcore/contrib/directory/migrations/0011_person_description_person_i18n.py +24 -0
  68. wbcore/contrib/directory/migrations/0012_alter_person_managers.py +20 -0
  69. wbcore/contrib/directory/migrations/0013_alter_clientmanagerrelationship_options.py +17 -0
  70. wbcore/contrib/directory/models/contacts.py +2 -2
  71. wbcore/contrib/directory/models/entries.py +31 -5
  72. wbcore/contrib/directory/models/relationships.py +31 -35
  73. wbcore/contrib/directory/permissions.py +6 -0
  74. wbcore/contrib/directory/serializers/companies.py +16 -8
  75. wbcore/contrib/directory/serializers/contacts.py +8 -8
  76. wbcore/contrib/directory/serializers/entries.py +26 -15
  77. wbcore/contrib/directory/serializers/entry_representations.py +4 -2
  78. wbcore/contrib/directory/serializers/persons.py +12 -10
  79. wbcore/contrib/directory/serializers/relationships.py +2 -2
  80. wbcore/contrib/directory/tests/conftest.py +2 -0
  81. wbcore/contrib/directory/tests/disable_signals.py +11 -1
  82. wbcore/contrib/directory/tests/signals.py +2 -2
  83. wbcore/contrib/directory/tests/test_models.py +88 -66
  84. wbcore/contrib/directory/tests/test_serializers.py +1 -1
  85. wbcore/contrib/directory/tests/test_viewsets.py +8 -8
  86. wbcore/contrib/directory/viewsets/buttons/__init__.py +1 -1
  87. wbcore/contrib/directory/viewsets/buttons/relationships.py +32 -0
  88. wbcore/contrib/directory/viewsets/contacts.py +6 -6
  89. wbcore/contrib/directory/viewsets/display/__init__.py +1 -1
  90. wbcore/contrib/directory/viewsets/display/contacts.py +1 -14
  91. wbcore/contrib/directory/viewsets/display/entries.py +68 -38
  92. wbcore/contrib/directory/viewsets/display/relationships.py +26 -50
  93. wbcore/contrib/directory/viewsets/endpoints/relationships.py +1 -26
  94. wbcore/contrib/directory/viewsets/entries.py +8 -6
  95. wbcore/contrib/directory/viewsets/previews/entries.py +3 -3
  96. wbcore/contrib/directory/viewsets/relationships.py +16 -2
  97. wbcore/contrib/directory/viewsets/titles/relationships.py +2 -3
  98. wbcore/contrib/documents/filters.py +0 -2
  99. wbcore/contrib/documents/locale/de/LC_MESSAGES/django.po +103 -94
  100. wbcore/contrib/documents/locale/de/LC_MESSAGES/django.po.translated +285 -0
  101. wbcore/contrib/documents/locale/en/LC_MESSAGES/django.po +271 -0
  102. wbcore/contrib/documents/locale/fr/LC_MESSAGES/django.po +270 -0
  103. wbcore/contrib/documents/tests/test_models.py +32 -28
  104. wbcore/contrib/documents/viewsets/endpoints/shareable_links.py +2 -21
  105. wbcore/contrib/dynamic_preferences/types.py +108 -0
  106. wbcore/contrib/dynamic_preferences/viewsets.py +27 -0
  107. wbcore/contrib/example_app/filters/event.py +3 -1
  108. wbcore/contrib/example_app/filters/match.py +1 -1
  109. wbcore/contrib/example_app/models.py +91 -22
  110. wbcore/contrib/example_app/serializers/person_team.py +4 -4
  111. wbcore/contrib/example_app/templates/example_app/embedded_view.html +19 -0
  112. wbcore/contrib/example_app/tests/e2e/test_teams.py +1 -1
  113. wbcore/contrib/example_app/tests/test_models/test_match.py +17 -7
  114. wbcore/contrib/example_app/urls.py +2 -0
  115. wbcore/contrib/example_app/views.py +7 -0
  116. wbcore/contrib/example_app/viewsets/displays/team.py +23 -4
  117. wbcore/contrib/example_app/viewsets/menu/menus.py +1 -1
  118. wbcore/contrib/example_app/viewsets/menus.py +1 -1
  119. wbcore/contrib/geography/tests/conftest.py +14 -0
  120. wbcore/contrib/geography/tests/test_models.py +23 -8
  121. wbcore/contrib/geography/tests/test_viewsets.py +96 -2
  122. wbcore/contrib/guardian/tests/test_model_mixins.py +3 -4
  123. wbcore/contrib/guardian/tests/test_tasks.py +9 -9
  124. wbcore/contrib/guardian/tests/test_viewsets.py +2 -2
  125. wbcore/contrib/guardian/viewsets/configs/__init__.py +1 -1
  126. wbcore/contrib/guardian/viewsets/configs/buttons.py +9 -0
  127. wbcore/contrib/guardian/viewsets/configs/endpoints.py +7 -0
  128. wbcore/contrib/guardian/viewsets/viewsets.py +2 -0
  129. wbcore/contrib/i18n/__init__.py +2 -0
  130. wbcore/contrib/i18n/buttons.py +33 -0
  131. wbcore/contrib/i18n/serializers/__init__.py +0 -0
  132. wbcore/contrib/i18n/serializers/fields.py +20 -0
  133. wbcore/contrib/i18n/serializers/mixins.py +13 -0
  134. wbcore/contrib/i18n/tests/conftest.py +11 -0
  135. wbcore/contrib/i18n/tests/test_viewsets.py +67 -0
  136. wbcore/contrib/i18n/translation.py +140 -0
  137. wbcore/contrib/i18n/viewsets.py +36 -0
  138. wbcore/contrib/icons/backends/default.py +1 -0
  139. wbcore/contrib/icons/backends/material.py +1 -0
  140. wbcore/contrib/icons/icons.py +5 -8
  141. wbcore/contrib/io/admin.py +1 -0
  142. wbcore/contrib/io/backends/mail.py +3 -2
  143. wbcore/contrib/io/backends/utils.py +14 -17
  144. wbcore/contrib/io/exceptions.py +8 -0
  145. wbcore/contrib/io/factories.py +1 -1
  146. wbcore/contrib/io/import_export/backends/mail.py +1 -0
  147. wbcore/contrib/io/import_export/backends/sftp.py +29 -20
  148. wbcore/contrib/io/import_export/backends/stream.py +2 -2
  149. wbcore/contrib/io/import_export/parsers/__init__.py +0 -0
  150. wbcore/contrib/io/import_export/parsers/base_csv.py +36 -0
  151. wbcore/contrib/io/import_export/parsers/resources.py +50 -0
  152. wbcore/contrib/io/imports.py +33 -25
  153. wbcore/contrib/io/locale/de/LC_MESSAGES/django.po +114 -22
  154. wbcore/contrib/io/locale/de/LC_MESSAGES/django.po.translated +103 -0
  155. wbcore/contrib/io/locale/en/LC_MESSAGES/django.po +138 -0
  156. wbcore/contrib/io/locale/fr/LC_MESSAGES/django.po +138 -0
  157. wbcore/contrib/io/migrations/0008_importsource_resource_kwargs.py +18 -0
  158. wbcore/contrib/io/models.py +65 -45
  159. wbcore/contrib/io/resources.py +0 -6
  160. wbcore/contrib/io/serializers.py +2 -2
  161. wbcore/contrib/io/signals.py +4 -0
  162. wbcore/contrib/io/tests/test_backends.py +19 -13
  163. wbcore/contrib/io/tests/test_exports.py +1 -1
  164. wbcore/contrib/io/tests/test_imports.py +1 -1
  165. wbcore/contrib/io/tests/test_models.py +47 -14
  166. wbcore/contrib/io/tests/test_viewsets.py +271 -0
  167. wbcore/contrib/io/viewset_mixins.py +41 -54
  168. wbcore/contrib/notifications/admin.py +1 -0
  169. wbcore/contrib/notifications/apps.py +2 -1
  170. wbcore/contrib/notifications/backends/abstract_backend.py +2 -4
  171. wbcore/contrib/notifications/backends/firebase/backends.py +5 -2
  172. wbcore/contrib/notifications/dispatch.py +18 -7
  173. wbcore/contrib/notifications/factories/notification_types.py +1 -0
  174. wbcore/contrib/notifications/locale/de/LC_MESSAGES/django.po +25 -19
  175. wbcore/contrib/notifications/locale/de/LC_MESSAGES/django.po.translated +63 -0
  176. wbcore/contrib/notifications/locale/en/LC_MESSAGES/django.po +61 -0
  177. wbcore/contrib/notifications/locale/fr/LC_MESSAGES/django.po +62 -0
  178. wbcore/contrib/notifications/migrations/0008_notificationtype_is_lock.py +18 -0
  179. wbcore/contrib/notifications/migrations/0009_alter_notificationtypesetting_options_and_more.py +32 -0
  180. wbcore/contrib/notifications/models/notification_types.py +67 -24
  181. wbcore/contrib/notifications/serializers/notification_types.py +16 -1
  182. wbcore/contrib/notifications/tests/test_models/test_tokens.py +8 -0
  183. wbcore/contrib/notifications/tests/test_serializers/test_notification_types.py +5 -0
  184. wbcore/contrib/notifications/tests/test_viewsets/test_notification_types.py +3 -5
  185. wbcore/contrib/notifications/utils.py +3 -2
  186. wbcore/contrib/notifications/viewsets/configs/notification_types.py +28 -6
  187. wbcore/contrib/notifications/viewsets/menus.py +1 -1
  188. wbcore/contrib/notifications/viewsets/notification_types.py +12 -2
  189. wbcore/contrib/pandas/fields.py +38 -10
  190. wbcore/contrib/pandas/filters.py +4 -1
  191. wbcore/contrib/pandas/filterset.py +8 -7
  192. wbcore/contrib/pandas/tests/test_fields/test_number_fields.py +2 -7
  193. wbcore/contrib/pandas/utils.py +1 -1
  194. wbcore/contrib/pandas/views.py +14 -13
  195. wbcore/contrib/tags/models/tags.py +4 -1
  196. wbcore/contrib/workflow/factories/display.py +2 -2
  197. wbcore/contrib/workflow/factories/transition.py +16 -15
  198. wbcore/contrib/workflow/locale/de/LC_MESSAGES/django.po +457 -566
  199. wbcore/contrib/workflow/locale/de/LC_MESSAGES/django.po.translated +1326 -0
  200. wbcore/contrib/workflow/locale/en/LC_MESSAGES/django.po +1102 -0
  201. wbcore/contrib/workflow/locale/fr/LC_MESSAGES/django.po +1114 -0
  202. wbcore/contrib/workflow/models/data.py +7 -4
  203. wbcore/contrib/workflow/models/process.py +2 -2
  204. wbcore/contrib/workflow/models/step.py +57 -15
  205. wbcore/contrib/workflow/serializers/data.py +8 -8
  206. wbcore/contrib/workflow/serializers/process.py +3 -2
  207. wbcore/contrib/workflow/tests/conftest.py +224 -0
  208. wbcore/contrib/workflow/tests/test_dispatch.py +82 -77
  209. wbcore/contrib/workflow/tests/test_displays.py +10 -88
  210. wbcore/contrib/workflow/tests/test_filters.py +57 -40
  211. wbcore/contrib/workflow/tests/test_models/step/test_decision_step.py +71 -68
  212. wbcore/contrib/workflow/tests/test_models/step/test_email_step.py +78 -38
  213. wbcore/contrib/workflow/tests/test_models/step/test_finish_step.py +152 -90
  214. wbcore/contrib/workflow/tests/test_models/step/test_join_step.py +100 -110
  215. wbcore/contrib/workflow/tests/test_models/step/test_step.py +168 -33
  216. wbcore/contrib/workflow/tests/test_models/test_condition.py +1 -1
  217. wbcore/contrib/workflow/tests/test_models/test_workflow.py +3 -3
  218. wbcore/contrib/workflow/tests/test_serializers.py +172 -150
  219. wbcore/contrib/workflow/tests/test_viewsets.py +264 -323
  220. wbcore/contrib/workflow/tests/test_workflow_assignees.py +215 -205
  221. wbcore/contrib/workflow/viewsets/process.py +4 -1
  222. wbcore/contrib/workflow/workflows/assignees.py +12 -7
  223. wbcore/dynamic_preferences_registry.py +102 -0
  224. wbcore/enums.py +2 -51
  225. wbcore/filters/fields/choices.py +4 -6
  226. wbcore/filters/fields/content_type.py +15 -4
  227. wbcore/filters/fields/datetime.py +50 -25
  228. wbcore/filters/fields/models.py +18 -9
  229. wbcore/filters/fields/numbers.py +9 -8
  230. wbcore/filters/filterset.py +27 -6
  231. wbcore/filters/mixins.py +41 -42
  232. wbcore/forms.py +6 -6
  233. wbcore/fsm/markdown_extensions.py +1 -1
  234. wbcore/fsm/mixins.py +20 -6
  235. wbcore/locale/de/LC_MESSAGES/django.po +982 -397
  236. wbcore/locale/de/LC_MESSAGES/django.po.translated +1580 -0
  237. wbcore/locale/en/LC_MESSAGES/django.po +1234 -0
  238. wbcore/locale/fr/LC_MESSAGES/django.po +1235 -0
  239. wbcore/markdown/models.py +8 -5
  240. wbcore/markdown/views.py +1 -1
  241. wbcore/menus/menus.py +2 -2
  242. wbcore/metadata/configs/buttons/bases.py +10 -7
  243. wbcore/metadata/configs/buttons/buttons.py +2 -1
  244. wbcore/metadata/configs/buttons/enums.py +50 -0
  245. wbcore/metadata/configs/buttons/view_config.py +13 -46
  246. wbcore/metadata/configs/display/display.py +2 -2
  247. wbcore/metadata/configs/display/formatting.py +6 -9
  248. wbcore/metadata/configs/display/instance_display/display.py +5 -2
  249. wbcore/metadata/configs/display/instance_display/pages.py +1 -1
  250. wbcore/metadata/configs/display/instance_display/shortcuts.py +1 -1
  251. wbcore/metadata/configs/display/list_display.py +54 -40
  252. wbcore/metadata/configs/display/models.py +6 -0
  253. wbcore/metadata/configs/display/view_config.py +11 -9
  254. wbcore/metadata/configs/endpoints.py +11 -4
  255. wbcore/metadata/configs/fields.py +6 -1
  256. wbcore/metadata/configs/filter_fields.py +12 -13
  257. wbcore/metadata/configs/identifiers.py +3 -1
  258. wbcore/metadata/tests/test_buttons.py +13 -16
  259. wbcore/models/fields.py +2 -2
  260. wbcore/pagination.py +1 -2
  261. wbcore/permissions/permissions.py +2 -2
  262. wbcore/permissions/utils.py +2 -2
  263. wbcore/release_notes/display.py +2 -8
  264. wbcore/release_notes/serializers.py +2 -9
  265. wbcore/release_notes/viewsets.py +8 -2
  266. wbcore/reversion/viewsets/titles.py +4 -3
  267. wbcore/serializers/__init__.py +2 -0
  268. wbcore/serializers/fields/__init__.py +2 -1
  269. wbcore/serializers/fields/boolean.py +1 -1
  270. wbcore/serializers/fields/choice.py +28 -4
  271. wbcore/serializers/fields/datetime.py +45 -36
  272. wbcore/serializers/fields/fields.py +1 -1
  273. wbcore/serializers/fields/fsm.py +1 -1
  274. wbcore/serializers/fields/list.py +2 -5
  275. wbcore/serializers/fields/mixins.py +24 -11
  276. wbcore/serializers/fields/number.py +6 -23
  277. wbcore/serializers/fields/other.py +2 -10
  278. wbcore/serializers/fields/related.py +4 -6
  279. wbcore/serializers/fields/text.py +1 -1
  280. wbcore/serializers/fields/types.py +2 -0
  281. wbcore/serializers/serializers.py +12 -3
  282. wbcore/signals/__init__.py +1 -0
  283. wbcore/signals/clone.py +4 -0
  284. wbcore/signals/models.py +2 -6
  285. wbcore/tasks.py +2 -2
  286. wbcore/templates/wbcore/email_base_template.html +3 -3
  287. wbcore/test/e2e_helpers_methods/e2e_checks.py +10 -4
  288. wbcore/test/e2e_helpers_methods/e2e_helper_methods.py +4 -2
  289. wbcore/test/mixins.py +52 -102
  290. wbcore/test/tests.py +6 -9
  291. wbcore/test/utils.py +3 -4
  292. wbcore/tests/e2e/test_e2e.py +2 -2
  293. wbcore/tests/test_cache/test_decorators.py +4 -7
  294. wbcore/tests/test_configs.py +2 -5
  295. wbcore/tests/test_enums.py +2 -1
  296. wbcore/tests/test_fields/test_choice_fields.py +9 -1
  297. wbcore/tests/test_fields/test_number_fields.py +7 -15
  298. wbcore/tests/test_fields/test_other_fields.py +1 -2
  299. wbcore/tests/test_filters/test_mixins.py +35 -35
  300. wbcore/tests/test_list_display.py +0 -2
  301. wbcore/tests/test_models/test_mixins.py +1 -1
  302. wbcore/tests/test_utils/test_date.py +1 -1
  303. wbcore/tests/test_utils/test_date_builder.py +25 -1
  304. wbcore/tests/test_utils/test_primary.py +1 -1
  305. wbcore/urls.py +4 -0
  306. wbcore/utils/date.py +18 -2
  307. wbcore/utils/figures.py +2 -2
  308. wbcore/utils/models.py +21 -4
  309. wbcore/utils/reportlab.py +7 -0
  310. wbcore/utils/rrules.py +3 -1
  311. wbcore/utils/string_loader.py +1 -1
  312. wbcore/utils/strings.py +3 -3
  313. wbcore/utils/views.py +8 -3
  314. wbcore/viewsets/mixins.py +9 -4
  315. {wbcore-1.46.0.dist-info → wbcore-1.58.2.dist-info}/METADATA +9 -5
  316. {wbcore-1.46.0.dist-info → wbcore-1.58.2.dist-info}/RECORD +317 -271
  317. wbcore/contrib/geography/tests/test_serializers.py +0 -7
  318. wbcore/contrib/geography/tests/tests.py +0 -13
  319. wbcore/contrib/io/tests/tests.py +0 -19
  320. wbcore/contrib/workflow/tests/tests.py +0 -25
  321. {wbcore-1.46.0.dist-info → wbcore-1.58.2.dist-info}/WHEEL +0 -0
wbcore/markdown/models.py CHANGED
@@ -17,7 +17,15 @@ class Asset(models.Model):
17
17
  file = models.FileField(max_length=256, upload_to=upload_to)
18
18
  content_type = models.CharField(max_length=32, null=True, blank=True)
19
19
  file_url_name = models.CharField(max_length=1024, null=True, blank=True)
20
+
20
21
  # public = models.BooleanField(default=True)
22
+ class Meta:
23
+ verbose_name = _("Asset")
24
+ verbose_name_plural = _("Assets")
25
+ db_table = "bridger_asset"
26
+
27
+ def __str__(self) -> str:
28
+ return str(self.id)
21
29
 
22
30
  @property
23
31
  def filename(self):
@@ -25,11 +33,6 @@ class Asset(models.Model):
25
33
  return f"{self.id}{suffix}"
26
34
  return self.id
27
35
 
28
- class Meta:
29
- verbose_name = _("Asset")
30
- verbose_name_plural = _("Assets")
31
- db_table = "bridger_asset"
32
-
33
36
 
34
37
  @receiver(models.signals.pre_save, sender="wbcore.Asset")
35
38
  def generate_content_type(sender, instance, **kwargs):
wbcore/markdown/views.py CHANGED
@@ -48,7 +48,7 @@ class AssetCreateView(APIView):
48
48
  try:
49
49
  asset = Asset.objects.create(file=request.data["file"])
50
50
  return Response(reverse("wbcore:asset-retrieve", args=[asset.id], request=request))
51
- except KeyError:
51
+ except (KeyError, AttributeError):
52
52
  return Response("file missing", status=400)
53
53
 
54
54
 
wbcore/menus/menus.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from dataclasses import dataclass, field
2
- from typing import Callable, Dict, List, Optional, Union
2
+ from typing import Any, Callable, Dict, List, Optional, Union
3
3
 
4
4
  from django.utils.http import urlencode
5
5
  from rest_framework.request import Request
@@ -32,7 +32,7 @@ class MenuItem:
32
32
  endpoint: str
33
33
  endpoint_args: List[str] = field(default_factory=list)
34
34
  endpoint_kwargs: Dict[str, str] = field(default_factory=dict)
35
- endpoint_get_parameters: Dict[str, str] = field(default_factory=dict)
35
+ endpoint_get_parameters: Callable | Dict[str, Any] = field(default_factory=dict)
36
36
  reverse: bool = True
37
37
 
38
38
  permission: Optional[ItemPermission] = None
@@ -24,8 +24,8 @@ class ButtonConfig:
24
24
  def __post_init__(self):
25
25
  if post_init := getattr(super(), "__post_init__", None):
26
26
  post_init()
27
-
28
- assert self.label or self.icon, "Either label or icon has to be defined."
27
+ if not self.label and not self.icon:
28
+ raise ValueError("No label or icon specified")
29
29
 
30
30
  def __iter__(self):
31
31
  if iter := getattr(super(), "__iter__", None):
@@ -37,7 +37,10 @@ class ButtonConfig:
37
37
  yield key, value
38
38
  color = getattr(self.color, "value", self.color)
39
39
  yield "color", color
40
- yield "level", color # TODO: we return level for backward compatibility reason. to be removed once we move to frontend version 2
40
+ yield (
41
+ "level",
42
+ color,
43
+ ) # TODO: we return level for backward compatibility reason. to be removed once we move to frontend version 2
41
44
  yield "disabled", self.disabled # set to True if you want to set the css "disabled" property to that button
42
45
  yield "always_render", self.always_render # set to True the button always needs to be rendered (even if empty)
43
46
  yield "placeholder", self.placeholder # set to a valid string if a placeholder is needed onhover
@@ -54,8 +57,8 @@ class ButtonTypeMixin:
54
57
  def __post_init__(self):
55
58
  if post_init := getattr(super(), "__post_init__", None):
56
59
  post_init()
57
-
58
- assert hasattr(self, "button_type"), "button_type cannot be None."
60
+ if not hasattr(self, "button_type"):
61
+ raise TypeError("button_type cannot be None.")
59
62
 
60
63
  def __iter__(self):
61
64
  if iter := getattr(super(), "__iter__", None):
@@ -79,8 +82,8 @@ class ButtonUrlMixin:
79
82
  def __post_init__(self):
80
83
  if post_init := getattr(super(), "__post_init__", None):
81
84
  post_init()
82
-
83
- assert bool(self.key) != bool(self.endpoint), "Either key or endpoint has to be defined. (Not both)"
85
+ if bool(self.key) == bool(self.endpoint):
86
+ raise ValueError("Either key or endpoint has to be defined. (Not both)")
84
87
 
85
88
  def __iter__(self):
86
89
  if iter := getattr(super(), "__iter__", None):
@@ -23,7 +23,8 @@ class DropDownButton(ButtonTypeMixin, ButtonConfig):
23
23
  if hasattr(super(), "__post_init__"):
24
24
  super().__post_init__()
25
25
  self.buttons = tuple(self.buttons)
26
- assert isinstance(self.buttons, tuple)
26
+ if not isinstance(self.buttons, tuple):
27
+ raise TypeError(f"{type(self.buttons)} is not a tuple")
27
28
 
28
29
  def serialize(self, request, **kwargs):
29
30
  res = super().serialize(request, **kwargs)
@@ -1,6 +1,56 @@
1
1
  from enum import Enum
2
2
 
3
3
 
4
+ class Button(Enum):
5
+ # Buttons
6
+ REFRESH = "refresh"
7
+ NEW = "new"
8
+ DELETE = "delete"
9
+
10
+ # Buttons and Create Buttons
11
+ SAVE = "save"
12
+ SAVE_AND_CLOSE = "save_and_close"
13
+ SAVE_AND_NEW = "save_and_new"
14
+
15
+ # Create Buttons
16
+ RESET = "reset"
17
+
18
+ # Custom Buttons
19
+ DROPDOWN = "dropdown"
20
+ HYPERLINK = "hyperlink"
21
+ WIDGET = "widget"
22
+ ACTION = "action"
23
+
24
+ @classmethod
25
+ def buttons(cls):
26
+ return [
27
+ cls.REFRESH.value,
28
+ cls.NEW.value,
29
+ cls.DELETE.value,
30
+ cls.SAVE.value,
31
+ cls.SAVE_AND_CLOSE.value,
32
+ cls.SAVE_AND_NEW.value,
33
+ ]
34
+
35
+ @classmethod
36
+ def create_buttons(cls):
37
+ return [
38
+ cls.SAVE.value,
39
+ cls.SAVE_AND_CLOSE.value,
40
+ cls.SAVE_AND_NEW.value,
41
+ cls.RESET.value,
42
+ ]
43
+
44
+ @classmethod
45
+ def custom_buttons(cls):
46
+ return [
47
+ cls.DROPDOWN.value,
48
+ cls.HYPERLINK.value,
49
+ cls.WIDGET.value,
50
+ cls.ACTION.value,
51
+ ]
52
+
53
+
4
54
  class ButtonDefaultColor(Enum):
5
55
  SUCCESS = "success"
6
56
  ERROR = "error"
@@ -1,8 +1,6 @@
1
- from contextlib import suppress
2
- from typing import Generator
1
+ from typing import Generator, Iterable
3
2
 
4
3
  from wbcore.contrib.icons import WBIcon
5
- from wbcore.enums import Button
6
4
  from wbcore.metadata.configs.buttons.buttons import DropDownButton
7
5
  from wbcore.signals.instance_buttons import (
8
6
  add_button,
@@ -12,9 +10,12 @@ from wbcore.signals.instance_buttons import (
12
10
  from wbcore.utils.importlib import parse_signal_received_for_module
13
11
 
14
12
  from ..base import WBCoreViewConfig
13
+ from .enums import Button
15
14
 
16
15
 
17
16
  class ButtonViewConfig(WBCoreViewConfig):
17
+ SHOW_INLINE: bool = False # set to true if the class needs to show custom button on inline
18
+
18
19
  metadata_key = "buttons"
19
20
  config_class_attribute = "button_config_class"
20
21
 
@@ -28,7 +29,9 @@ class ButtonViewConfig(WBCoreViewConfig):
28
29
  Returns:
29
30
  Yield the serialized button, without duplicates and appends the module prefix to the remote button
30
31
  """
31
- base_buttons = list(zip([None] * len(base_buttons), base_buttons)) # append an empty perfix for base buttons
32
+ base_buttons = list(
33
+ zip([None] * len(base_buttons), base_buttons, strict=False)
34
+ ) # append an empty perfix for base buttons
32
35
  for prefix, btn in parse_signal_received_for_module(remote_resources):
33
36
  base_buttons.append((prefix, btn))
34
37
 
@@ -53,57 +56,20 @@ class ButtonViewConfig(WBCoreViewConfig):
53
56
  FSM_WEIGHT = 100
54
57
 
55
58
  def get_fsm_buttons(self) -> set:
56
- if self.FSM_DROPDOWN and (FSM_BUTTONS := getattr(self.view, "FSM_BUTTONS")) and len(FSM_BUTTONS) > 0:
59
+ if self.FSM_DROPDOWN and (fsm_buttons := self.view.FSM_BUTTONS) and len(fsm_buttons) > 0:
57
60
  return {
58
61
  DropDownButton(
59
62
  label=self.FSM_DROPDOWN_LABEL,
60
63
  icon=self.FSM_DROPDOWN_ICON,
61
64
  title=self.FSM_DROPDOWN_LABEL,
62
65
  weight=self.FSM_WEIGHT,
63
- buttons=tuple(FSM_BUTTONS),
66
+ buttons=tuple(fsm_buttons),
64
67
  )
65
68
  }
66
69
  return getattr(self.view, "FSM_BUTTONS", set())
67
70
 
68
- # list Button Configuration
69
- LIST_BUTTONS = frozenset({Button.NEW.value, Button.REFRESH.value})
70
- LIST_BUTTONS_ORDERING = [Button.NEW.value, Button.REFRESH.value]
71
-
72
- def get_list_buttons(self) -> Generator[None, str, None]:
73
- if content_type := self.view.get_content_type():
74
- buttons = set(self.LIST_BUTTONS)
75
- add_permission = f"{content_type.app_label}.add_{content_type.model}"
76
-
77
- if not self.request.user.has_perm(add_permission):
78
- with suppress(KeyError):
79
- buttons.remove(Button.NEW.value)
80
-
81
- yield from self.order_buttons(buttons, self.LIST_BUTTONS_ORDERING)
82
-
83
- # Instance Button Configuration
84
- INSTANCE_BUTTONS = frozenset({Button.SAVE.value, Button.REFRESH.value, Button.DELETE.value})
85
- INSTANCE_BUTTONS_ORDERING = [Button.SAVE.value, Button.REFRESH.value, Button.DELETE.value]
86
-
87
- def get_instance_buttons(self) -> Generator[None, str, None]:
88
- if content_type := self.view.get_content_type():
89
- buttons = set(self.INSTANCE_BUTTONS)
90
- change_permission = f"{content_type.app_label}.change_{content_type.model}"
91
- delete_permission = f"{content_type.app_label}.delete_{content_type.model}"
92
-
93
- if not self.request.user.has_perm(change_permission):
94
- with suppress(KeyError):
95
- buttons.remove(Button.SAVE.value)
96
-
97
- if not self.request.user.has_perm(delete_permission):
98
- with suppress(KeyError):
99
- buttons.remove(Button.DELETE.value)
100
-
101
- yield from self.order_buttons(buttons, self.INSTANCE_BUTTONS_ORDERING)
102
-
103
71
  # Create Button Configuration
104
- CREATE_BUTTONS = frozenset(
105
- {Button.SAVE.value, Button.SAVE_AND_CLOSE.value, Button.SAVE_AND_NEW.value, Button.RESET.value}
106
- )
72
+ CREATE_BUTTONS = frozenset({Button.SAVE.value})
107
73
  CREATE_BUTTONS_ORDERING = [
108
74
  Button.SAVE.value,
109
75
  Button.SAVE_AND_CLOSE.value,
@@ -111,7 +77,7 @@ class ButtonViewConfig(WBCoreViewConfig):
111
77
  Button.RESET.value,
112
78
  ]
113
79
 
114
- def get_create_buttons(self) -> Generator[None, str, None]:
80
+ def get_create_buttons(self) -> Iterable:
115
81
  buttons = set(self.CREATE_BUTTONS)
116
82
  yield from self.order_buttons(buttons, self.CREATE_BUTTONS_ORDERING)
117
83
 
@@ -172,7 +138,8 @@ class ButtonViewConfig(WBCoreViewConfig):
172
138
  return {
173
139
  "custom_instance": self._get_custom_instance_buttons(),
174
140
  "custom": self._get_custom_buttons()
175
- if not self.view.inline
141
+ if (not self.view.inline or self.SHOW_INLINE)
176
142
  else [], # we do not show button for list display generated through inline
177
143
  "extra": self._get_custom_extra_buttons(),
144
+ "save": self.get_create_buttons(),
178
145
  }
@@ -41,8 +41,8 @@ class Operator(Enum):
41
41
  operator_dict = {o.value: o for o in cls}
42
42
  try:
43
43
  return operator_dict[op]
44
- except KeyError:
45
- raise InvalidOperatorError(f"`{op}` is not a valid operator")
44
+ except KeyError as e:
45
+ raise InvalidOperatorError(f"`{op}` is not a valid operator") from e
46
46
 
47
47
 
48
48
  def fr(fractions: int) -> str:
@@ -9,21 +9,20 @@ class Condition:
9
9
  value: str | float | int | bool
10
10
 
11
11
  def __post_init__(self) -> None:
12
- if self.operator == Operator.EXISTS:
13
- assert isinstance(self.value, bool), f"{Operator.EXISTS.value} is only compatible with bool"
12
+ if self.operator == Operator.EXISTS and not isinstance(self.value, bool):
13
+ raise TypeError(f"{Operator.EXISTS.value} is only compatible with bool")
14
14
 
15
15
 
16
16
  @dataclass(unsafe_hash=True)
17
17
  class FormattingRule:
18
- icon: str | None = None
19
18
  style: dict | None = None
20
19
  condition: Condition | tuple | list[tuple] | None = None
21
20
 
22
21
  def __post_init__(self) -> None:
23
- assert self.icon or self.style, "icon and style cannot both be None."
22
+ if not self.style:
23
+ raise ValueError("Style cannot be empty")
24
24
 
25
25
  def __iter__(self):
26
- yield "icon", self.icon
27
26
  yield "style", self.style
28
27
  if self.condition:
29
28
  if isinstance(self.condition, tuple):
@@ -40,10 +39,8 @@ class Formatting:
40
39
  column: str | None = None
41
40
 
42
41
  def __post_init__(self) -> None:
43
- if self.column is None:
44
- assert all(
45
- [not bool(rule.condition) for rule in self.formatting_rules]
46
- ), "Specifying conditions, without a reference column is not possible."
42
+ if self.column is None and not all([not bool(rule.condition) for rule in self.formatting_rules]):
43
+ raise ValueError("Specifying conditions, without a reference column is not possible.")
47
44
 
48
45
  def __iter__(self):
49
46
  yield "column", self.column
@@ -17,7 +17,9 @@ class Display:
17
17
  pages: Iterable[Page]
18
18
  navigation_type: NavigationType = NavigationType.TAB
19
19
 
20
- def serialize(self, view_config=None, view=None, request=None, key_prefix=None) -> SerializedDisplay:
20
+ def serialize(
21
+ self, view_config=None, view=None, request=None, key_prefix=None, parent_page=None
22
+ ) -> SerializedDisplay:
21
23
  """Serializes a `Display`
22
24
 
23
25
  Returns:
@@ -28,7 +30,8 @@ class Display:
28
30
  page.serialize(key_prefix=key_prefix, view_config=view_config, view=view, request=request)
29
31
  for page in self.pages
30
32
  ]
31
- if view_config:
33
+ # we only allow remote page registration if the display is the top display. We could argue that we want to allow dev to register pages for any nested display by passing down the page argument. This involves refactoring of the signal receivers
34
+ if view_config and parent_page is None:
32
35
  for prefix, remote_pages in parse_signal_received_for_module(
33
36
  add_display_pages.send(view_config.__class__, request=request, view=view)
34
37
  ):
@@ -34,7 +34,7 @@ class Page:
34
34
 
35
35
  elif self.display is not None:
36
36
  page["display"] = self.display.serialize(
37
- view_config=view_config, view=view, request=request, key_prefix=key_prefix
37
+ view_config=view_config, view=view, request=request, key_prefix=key_prefix, parent_page=self
38
38
  )
39
39
 
40
40
  return page
@@ -43,7 +43,7 @@ def create_simple_section(
43
43
  grid_template_areas: list[list[str]] | None = None,
44
44
  inline_key: str | None = None,
45
45
  extra_display_kwargs: dict | None = None,
46
- **kwargs
46
+ **kwargs,
47
47
  ) -> Section:
48
48
  """Creates a simple section without having to specify everything
49
49
 
@@ -1,6 +1,8 @@
1
1
  from dataclasses import dataclass, field
2
2
  from typing import Iterable, Literal, Optional
3
3
 
4
+ from slugify import slugify
5
+
4
6
  from wbcore.metadata.configs.display.formatting import Formatting, FormattingRule
5
7
 
6
8
 
@@ -34,7 +36,7 @@ class Tooltip:
34
36
 
35
37
  @dataclass(unsafe_hash=True)
36
38
  class Field:
37
- key: str
39
+ key: str | None
38
40
  label: str
39
41
  formatting_rules: Iterable[FormattingRule] = field(default_factory=list)
40
42
  width: int | None = None
@@ -56,6 +58,11 @@ class Field:
56
58
  menu: bool = True
57
59
  size_to_fit: bool = True
58
60
 
61
+ def __post_init__(self):
62
+ self.identifier = (
63
+ self.key if self.key else slugify(str(self.label))
64
+ ) # we cast to str explicitly in case label is in a translation wrapper
65
+
59
66
  def iterate_leaf_fields(self, aggregated_parent_label: str = ""):
60
67
  label = self.label
61
68
  if aggregated_parent_label:
@@ -66,50 +73,55 @@ class Field:
66
73
  else:
67
74
  yield self.key, label
68
75
 
69
- def __iter__(self):
70
- yield "key", self.key
71
- yield "label", self.label
72
- yield "formatting_rules", [dict(rule) for rule in self.formatting_rules]
76
+ def serialize(self, parent_identifier: str | None = None):
77
+ identifier = parent_identifier + "_" + self.identifier if parent_identifier else self.identifier
78
+ repr = {
79
+ "identifier": identifier,
80
+ "key": self.key,
81
+ "label": self.label,
82
+ "formatting_rules": [dict(rule) for rule in self.formatting_rules],
83
+ }
73
84
 
74
85
  if self.width:
75
- yield "width", self.width
86
+ repr["width"] = self.width
76
87
 
77
88
  if self.hide:
78
- yield "hide", self.hide
89
+ repr["hide"] = self.hide
79
90
 
80
91
  if self.pinned and self.pinned in ["left", "right"]:
81
- yield "pinned", self.pinned
92
+ repr["pinned"] = self.pinned
82
93
 
83
94
  if self.children:
84
- yield "children", [dict(child) for child in self.children]
85
- yield "marry_children", self.marry_children is True # Convert None into False
95
+ repr["children"] = [child.serialize(identifier) for child in self.children]
96
+ repr["marry_children"] = self.marry_children is True # Convert None into False
86
97
 
87
98
  if self.show:
88
- yield "show", self.show
99
+ repr["show"] = self.show
89
100
 
90
101
  if self.open_by_default is not None:
91
- yield "open_by_default", self.open_by_default
102
+ repr["open_by_default"] = self.open_by_default
92
103
 
93
104
  if not self.movable:
94
- yield "movable", self.movable
105
+ repr["movable"] = self.movable
95
106
 
96
107
  if not self.resizable:
97
- yield "resizable", self.resizable
108
+ repr["resizable"] = self.resizable
98
109
 
99
110
  if self.lock_position:
100
- yield "lock_position", self.lock_position
111
+ repr["lock_position"] = self.lock_position
101
112
 
102
113
  if not self.auto_size:
103
- yield "auto_size", self.auto_size
114
+ repr["auto_size"] = self.auto_size
104
115
 
105
116
  if not self.menu:
106
- yield "menu", self.menu
117
+ repr["menu"] = self.menu
107
118
 
108
119
  if not self.size_to_fit:
109
- yield "size_to_fit", self.size_to_fit
120
+ repr["size_to_fit"] = self.size_to_fit
110
121
 
111
122
  if self.tooltip:
112
- yield "tooltip", self.tooltip.serialize()
123
+ repr["tooltip"] = self.tooltip.serialize()
124
+ return repr
113
125
 
114
126
 
115
127
  @dataclass(unsafe_hash=True)
@@ -133,10 +145,8 @@ class Legend:
133
145
  key: str | None = None
134
146
 
135
147
  def __post_init__(self):
136
- if self.key:
137
- assert all(
138
- [item.value is not None for item in self.items]
139
- ), "If key is set, all items need to specify a value."
148
+ if self.key and not all([item.value is not None for item in self.items]):
149
+ raise ValueError("If key is set, all items need to specify a value.")
140
150
 
141
151
  def __iter__(self):
142
152
  if self.label:
@@ -148,11 +158,8 @@ class Legend:
148
158
  yield "items", [dict(item) for item in self.items]
149
159
 
150
160
 
151
- @dataclass(unsafe_hash=True)
152
- class TreeGroupLevelOption:
153
- list_endpoint: str
154
- reorder_endpoint: str | None = None
155
- reparent_endpoint: str | None = None
161
+ @dataclass(unsafe_hash=True, kw_only=True)
162
+ class BaseTreeGroupLevelOption:
156
163
  lookup: str = "_group_key"
157
164
  filter_key: str = "group_keys"
158
165
  filter_whitelist: list[str] = field(default_factory=list)
@@ -162,10 +169,26 @@ class TreeGroupLevelOption:
162
169
  # Set to True if preselected filters other than required ones need to be cleared out before fetching the tree group
163
170
  )
164
171
  filter_depth: int | None = 1 # None would actually return all group keys concatenated.
172
+
173
+ def __iter__(self):
174
+ yield "lookup", self.lookup
175
+ yield "filter_key", self.filter_key
176
+ yield "filter_whitelist", self.filter_whitelist
177
+ yield "filter_blacklist", self.filter_blacklist
178
+ yield "clear_filter", self.clear_filter
179
+ yield "filter_depth", self.filter_depth
180
+
181
+
182
+ @dataclass(unsafe_hash=True, kw_only=True)
183
+ class TreeGroupLevelOption(BaseTreeGroupLevelOption):
184
+ list_endpoint: str
185
+ reorder_endpoint: str | None = None
186
+ reparent_endpoint: str | None = None
165
187
  parent_field: str = "parent"
166
188
  ordering_field: str = "order"
167
189
 
168
190
  def __iter__(self):
191
+ yield from super().__iter__()
169
192
  endpoints = {"list": self.list_endpoint}
170
193
  if self.reorder_endpoint:
171
194
  endpoints["reorder"] = self.reorder_endpoint
@@ -174,12 +197,7 @@ class TreeGroupLevelOption:
174
197
  endpoints["reparent"] = self.reparent_endpoint
175
198
 
176
199
  yield "endpoints", endpoints
177
- yield "lookup", self.lookup
178
- yield "filter_key", self.filter_key
179
- yield "filter_whitelist", self.filter_whitelist
180
- yield "filter_blacklist", self.filter_blacklist
181
- yield "clear_filter", self.clear_filter
182
- yield "filter_depth", self.filter_depth
200
+
183
201
  yield "parent_field", self.parent_field
184
202
  yield "ordering_field", self.ordering_field
185
203
 
@@ -198,14 +216,12 @@ class ListDisplay:
198
216
  auto_height: bool = False
199
217
 
200
218
  tree: bool = False
201
- tree_group_pinned: str = "left"
202
219
  tree_group_parent_pointer: str | None = (
203
220
  None # if specified, the display assumes the whole tree data is given in the initial request and that it can be grouped by the given group key
204
221
  )
205
222
  tree_group_key: str = "_group_key" # The field the `tree_group_parent_pointer` points to
206
223
 
207
224
  tree_group_field: str | None = None
208
- tree_group_label: str | None = None
209
225
  tree_group_open_level: int = 0
210
226
  tree_group_field_sortable: bool = False
211
227
 
@@ -219,7 +235,7 @@ class ListDisplay:
219
235
 
220
236
  def __iter__(self):
221
237
  yield "editable", self.editable
222
- yield "fields", [dict(field) for field in self.fields if field]
238
+ yield "fields", [field.serialize() for field in self.fields if field]
223
239
  yield "legends", [dict(legend) for legend in self.legends if legend]
224
240
  yield "formatting", [dict(formatting) for formatting in self.formatting if formatting]
225
241
  yield "hide_control_bar", self.hide_control_bar
@@ -233,9 +249,7 @@ class ListDisplay:
233
249
  if self.tree:
234
250
  yield "tree", self.tree
235
251
  tree_group = {
236
- "pinned": self.tree_group_pinned,
237
- "field": self.tree_group_field if self.tree_group_field else self.fields[0].key,
238
- "label": self.tree_group_label if self.tree_group_label else self.fields[0].key,
252
+ "field": (self.tree_group_field if self.tree_group_field else self.fields[0].key),
239
253
  "open_level": self.tree_group_open_level,
240
254
  "field_sortable": self.tree_group_field_sortable,
241
255
  "group_key": self.tree_group_key,
@@ -10,6 +10,9 @@ class Preset(models.Model):
10
10
  display_identifier = models.CharField(max_length=512)
11
11
  display = models.JSONField(null=True, blank=True)
12
12
 
13
+ def __str__(self) -> str:
14
+ return f"{self.title} - {self.user} ({self.display_identifier})"
15
+
13
16
 
14
17
  class AppliedPreset(models.Model):
15
18
  user = models.ForeignKey(to=get_user_model(), related_name="applied_presets", on_delete=models.CASCADE)
@@ -18,3 +21,6 @@ class AppliedPreset(models.Model):
18
21
  to=Preset, related_name="applied_presets", on_delete=models.SET_NULL, null=True, blank=True
19
22
  )
20
23
  display = models.JSONField(null=True, blank=True)
24
+
25
+ def __str__(self) -> str:
26
+ return f"{self.display_identifier_path} ({self.user})"
@@ -53,13 +53,9 @@ class DisplayViewConfig(WBCoreViewConfig):
53
53
  if window := self.get_window():
54
54
  display["window"] = window.serialize()
55
55
 
56
- # We get the path from the header (if it exists, only for nested tables inside forms) and then join it
57
- # with the current display identifier. If there is an applied preset for this user - we return it.
58
- path = self.request.META.get("HTTP_WB_DISPLAY_IDENTIFIER", None)
59
- display_identifier = self.view.display_identifier_config_class(
56
+ display_identifier_path = self.view.display_identifier_config_class(
60
57
  self.view, self.request, self.instance
61
- ).get_display_identifier()
62
- display_identifier_path = ".".join(filter(lambda element: element is not None, [path, display_identifier]))
58
+ ).display_identifier_path()
63
59
 
64
60
  with suppress(AppliedPreset.DoesNotExist):
65
61
  display["preset"] = AppliedPreset.objects.get(
@@ -73,11 +69,17 @@ class DisplayIdentifierViewConfig(WBCoreViewConfig):
73
69
  metadata_key = "display_identifier"
74
70
  config_class_attribute = "display_identifier_config_class"
75
71
 
76
- def get_display_identifier(self) -> str:
72
+ def display_identifier_path(self) -> str:
77
73
  display = self.view.display_config_class
78
74
  slugified_display_module = slugify(display.__module__.replace(".", "-"))
79
75
  slugified_display_class = slugify(display.__name__)
80
- return f"{slugified_display_module}-{slugified_display_class}"
76
+ display_identifier_path = f"{slugified_display_module}-{slugified_display_class}"
77
+
78
+ # We get the path from the header (if it exists, only for nested tables inside forms) and then join it
79
+ # with the current display identifier. If there is an applied preset for this user - we return it.
80
+ if inline_path := self.request.META.get("HTTP_WB_DISPLAY_IDENTIFIER", None):
81
+ display_identifier_path = f"{inline_path}.{display_identifier_path}"
82
+ return display_identifier_path
81
83
 
82
84
  def get_metadata(self):
83
- return self.get_display_identifier()
85
+ return self.display_identifier_path()