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
@@ -1,10 +1,9 @@
1
- from unittest.mock import patch
2
-
3
1
  import pytest
2
+ from pytest_mock import MockerFixture
4
3
 
5
- from wbcore.contrib.authentication.factories import GroupFactory, UserFactory
6
- from wbcore.contrib.directory.factories import ClientFactory
7
- from wbcore.contrib.workflow.models import ProcessStep, Step
4
+ from wbcore.contrib.authentication.models import Group, User
5
+ from wbcore.contrib.directory.models import Entry
6
+ from wbcore.contrib.workflow.models import Process, ProcessStep, Step
8
7
  from wbcore.contrib.workflow.workflows import (
9
8
  manager_of_instance_assignee,
10
9
  random_group_member,
@@ -12,215 +11,226 @@ from wbcore.contrib.workflow.workflows import (
12
11
  )
13
12
 
14
13
 
15
- @pytest.mark.django_db
16
14
  class TestWorkflowAssignees:
17
- def test_manager_of_instance_assignee_person(self, process_step_factory):
18
- kwargs = {"assignee_field": "profile", "assignee_type": "entry"}
19
- attached_instance = UserFactory()
20
- process_step = process_step_factory(process__instance=attached_instance)
21
- manager_list = ClientFactory.create_batch(3, clients=[attached_instance.profile])
22
- UserFactory(profile=manager_list[1], is_active=False)
23
- UserFactory(profile=manager_list[2])
24
- assert manager_of_instance_assignee(process_step, **kwargs) == manager_list[2].user_account
25
-
26
- def test_manager_of_instance_assignee_no_kwargs(self, process_step_factory):
27
- process_step = process_step_factory()
28
- assert not manager_of_instance_assignee(process_step)
29
- assert process_step.state == ProcessStep.StepState.FAILED
30
-
31
- def test_manager_of_instance_assignee_no_assignee_field(self, process_step_factory):
32
- kwargs = {"test": "profile", "assignee_type": "entry"}
33
- process_step = process_step_factory()
34
- assert not manager_of_instance_assignee(process_step, **kwargs)
35
- assert process_step.state == ProcessStep.StepState.FAILED
15
+ @pytest.fixture
16
+ def mocked_active_user(self, mocker: MockerFixture):
17
+ return mocker.MagicMock(spec=User, is_active=True)
36
18
 
37
- def test_manager_of_instance_assignee_no_assignee_type(self, process_step_factory):
38
- kwargs = {"assignee_field": "profile", "test": "entry"}
39
- process_step = process_step_factory()
40
- assert not manager_of_instance_assignee(process_step, **kwargs)
41
- assert process_step.state == ProcessStep.StepState.FAILED
19
+ @pytest.fixture
20
+ def mocked_group(self, mocker: MockerFixture):
21
+ return mocker.MagicMock(spec=Group)
42
22
 
43
- def test_manager_of_instance_assignee_wrong_assignee_field(self, process_step_factory):
23
+ def test_manager_of_instance_assignee_person(self, mocker: MockerFixture, mocked_process_step, mocked_active_user):
24
+ # Arrange
44
25
  kwargs = {"assignee_field": "profile", "assignee_type": "entry"}
45
- process_step = process_step_factory()
46
- assert not manager_of_instance_assignee(process_step, **kwargs)
47
- assert process_step.state == ProcessStep.StepState.FAILED
48
26
 
49
- def test_manager_of_instance_assignee_wrong_assignee_type(self, process_step_factory):
50
- kwargs = {"assignee_field": "profile", "assignee_type": "test"}
51
- attached_instance = UserFactory()
52
- process_step = process_step_factory(process__instance=attached_instance)
53
- assert not manager_of_instance_assignee(process_step, **kwargs)
54
- assert process_step.state == ProcessStep.StepState.FAILED
55
-
56
- @pytest.mark.parametrize(
57
- "bad_states", [ProcessStep.StepState.FAILED, ProcessStep.StepState.CANCELED, ProcessStep.StepState.WAITING]
58
- )
59
- @pytest.mark.parametrize(
60
- "bad_types",
61
- [
62
- Step.StepType.DECISIONSTEP,
63
- Step.StepType.EMAILSTEP,
64
- Step.StepType.FINISHSTEP,
65
- Step.StepType.JOINSTEP,
66
- Step.StepType.SPLITSTEP,
67
- Step.StepType.SCRIPTSTEP,
68
- ],
69
- )
70
- @patch("wbcore.contrib.workflow.workflows.assignees.choices")
71
- def test_weighted_random(self, mock_choices, user_step_factory, process_step_factory, bad_states, bad_types):
72
- group = GroupFactory()
73
- process_step = process_step_factory(group=group)
74
- assignee1 = UserFactory(groups=[group])
75
- assignee2 = UserFactory(groups=[group])
76
- user_step = user_step_factory()
77
- process_step_factory(
78
- state=ProcessStep.StepState.ACTIVE,
79
- process=process_step.process,
80
- group=process_step.group,
81
- step=user_step,
82
- assignee=assignee1,
83
- )
84
- process_step_factory(
85
- state=ProcessStep.StepState.FINISHED,
86
- process=process_step.process,
87
- group=process_step.group,
88
- step=user_step,
89
- assignee=assignee2,
90
- )
91
- process_step_factory(
92
- state=ProcessStep.StepState.FINISHED,
93
- process=process_step.process,
94
- group=process_step.group,
95
- step=user_step,
96
- assignee=assignee2,
97
- )
98
- process_step_factory(
99
- state=bad_states,
100
- process=process_step.process,
101
- group=process_step.group,
102
- step=user_step,
103
- assignee=assignee2,
104
- )
105
- process_step_factory(
106
- state=ProcessStep.StepState.ACTIVE,
107
- group=process_step.group,
108
- step=user_step,
109
- assignee=assignee2,
110
- )
111
- process_step_factory(
112
- state=ProcessStep.StepState.ACTIVE,
113
- process=process_step.process,
114
- step=user_step,
115
- assignee=assignee2,
116
- )
117
- process_step_factory(
118
- state=ProcessStep.StepState.FINISHED,
119
- process=process_step.process,
120
- group=process_step.group,
121
- step__step_type=bad_types,
122
- assignee=assignee2,
27
+ user_a = mocker.MagicMock(spec=User, is_active=False)
28
+ manager_a = mocker.MagicMock(spec=Entry, user_account=user_a)
29
+ manager_b = mocker.MagicMock(spec=Entry, user_account=mocked_active_user)
30
+
31
+ assignee_mock = mocker.MagicMock()
32
+
33
+ mocked_process_step.process.instance = mocker.MagicMock(spec=User)
34
+ mocked_process_step.process.instance.profile = assignee_mock
35
+
36
+ assignee_mock.relationship_managers.all.return_value = [manager_a, manager_b]
37
+ # Act
38
+ result = manager_of_instance_assignee(mocked_process_step, **kwargs)
39
+ # Assert
40
+ assert result == manager_b.user_account
41
+
42
+ def test_manager_of_instance_assignee_no_kwargs(self, mocked_process_step, mocker: MockerFixture):
43
+ mocked_process_step.step.get_casted_step.return_value = mocker.MagicMock(spec=Step)
44
+ result = manager_of_instance_assignee(mocked_process_step)
45
+ assert result is None
46
+ mocked_process_step.step.get_casted_step().set_failed.assert_called_once_with(
47
+ mocked_process_step,
48
+ "Error in assignee method: Incorrect input in kwargs field!",
123
49
  )
124
- process_step_factory(
125
- state=ProcessStep.StepState.FINISHED,
126
- process=process_step.process,
127
- group=process_step.group,
128
- step=user_step,
50
+
51
+ def test_manager_of_instance_assignee_no_assignee_field(self, mocked_process_step):
52
+ kwargs = {"test": "profile", "assignee_type": "entry"}
53
+ result = manager_of_instance_assignee(mocked_process_step, **kwargs)
54
+ assert result is None
55
+ mocked_process_step.step.get_casted_step().set_failed.assert_called_once_with(
56
+ mocked_process_step,
57
+ "Error in assignee method: Incorrect input in kwargs field!",
129
58
  )
130
- assert weighted_random(process_step)
131
- assert len(mock_choices.call_args.args) == 1
132
- assert len(mock_choices.call_args.kwargs) == 1
133
- assignees_with_weights = set(zip(mock_choices.call_args.args[0], mock_choices.call_args.kwargs["weights"]))
134
- assert assignees_with_weights == {(assignee1, 2 / 3), (assignee2, 1 / 3)}
135
-
136
- @pytest.mark.parametrize(
137
- "bad_states", [ProcessStep.StepState.FAILED, ProcessStep.StepState.CANCELED, ProcessStep.StepState.WAITING]
138
- )
139
- @pytest.mark.parametrize(
140
- "bad_types",
141
- [
142
- Step.StepType.DECISIONSTEP,
143
- Step.StepType.EMAILSTEP,
144
- Step.StepType.FINISHSTEP,
145
- Step.StepType.JOINSTEP,
146
- Step.StepType.SPLITSTEP,
147
- Step.StepType.SCRIPTSTEP,
148
- ],
149
- )
150
- @patch("wbcore.contrib.workflow.workflows.assignees.choices")
151
- def test_weighted_random_no_past_ocurrences(
152
- self, mock_choices, user_step_factory, process_step_factory, bad_states, bad_types
153
- ):
154
- group = GroupFactory()
155
- process_step = process_step_factory(group=group)
156
- assignee1 = UserFactory(groups=[group])
157
- assignee2 = UserFactory(groups=[group])
158
- user_step = user_step_factory()
159
-
160
- process_step_factory(
161
- state=bad_states,
162
- process=process_step.process,
163
- group=process_step.group,
164
- step=user_step,
165
- assignee=assignee2,
59
+
60
+ def test_manager_of_instance_assignee_no_assignee_type(self, mocked_process_step):
61
+ kwargs = {"assignee_field": "profile", "test": "entry"}
62
+ result = manager_of_instance_assignee(mocked_process_step, **kwargs)
63
+ assert result is None
64
+ mocked_process_step.step.get_casted_step().set_failed.assert_called_once_with(
65
+ mocked_process_step,
66
+ "Error in assignee method: Incorrect input in kwargs field!",
166
67
  )
167
- process_step_factory(
168
- state=ProcessStep.StepState.ACTIVE,
169
- group=process_step.group,
170
- step=user_step,
171
- assignee=assignee2,
68
+
69
+ def test_manager_of_instance_assignee_wrong_assignee_field(self, mocked_process_step):
70
+ kwargs = {"assignee_field": "profile", "assignee_type": "entry"}
71
+ result = manager_of_instance_assignee(mocked_process_step, **kwargs)
72
+ assert result is None
73
+ mocked_process_step.step.get_casted_step().set_failed.assert_called_once_with(
74
+ mocked_process_step,
75
+ "Error in assignee method: Incorrect input in kwargs field!",
172
76
  )
173
- process_step_factory(
174
- state=ProcessStep.StepState.ACTIVE,
175
- process=process_step.process,
176
- step=user_step,
177
- assignee=assignee2,
77
+
78
+ def test_manager_of_instance_assignee_wrong_assignee_type(self, mocked_process_step, mocked_active_user):
79
+ kwargs = {"assignee_field": "profile", "assignee_type": "test"}
80
+ mocked_process_step.process.instance = mocked_active_user
81
+ result = manager_of_instance_assignee(mocked_process_step, **kwargs)
82
+ assert result is None
83
+ mocked_process_step.step.get_casted_step().set_failed.assert_called_once_with(
84
+ mocked_process_step,
85
+ "Error in assignee method: Incorrect input in kwargs field!",
178
86
  )
179
- process_step_factory(
180
- state=ProcessStep.StepState.FINISHED,
181
- process=process_step.process,
182
- group=process_step.group,
183
- step__step_type=bad_types,
184
- assignee=assignee2,
87
+
88
+ def test_weighted_random_with_prior_assignments(self, mocker: MockerFixture, mocked_process_step, mocked_group):
89
+ # Arrange
90
+ user_a = mocker.MagicMock(spec=User, pk=1)
91
+ user_b = mocker.MagicMock(spec=User, pk=2)
92
+ mock_group_users = [user_a, user_b]
93
+
94
+ mocked_group.user_set.all.return_value = mock_group_users
95
+ mocked_group.user_set.count.return_value = len(mock_group_users)
96
+
97
+ mocked_process_step.group = mocked_group
98
+ mocked_process_step.process = mocker.MagicMock(spec=Process)
99
+
100
+ mocked_choices = mocker.patch("wbcore.contrib.workflow.workflows.assignees.choices", return_value=[user_a])
101
+ mocked_qs = mocker.patch("wbcore.contrib.workflow.workflows.assignees.ProcessStep.objects.filter")
102
+
103
+ mocked_query = mocked_qs.return_value
104
+ mocked_query.exclude.return_value.values_list.return_value = [1, 2, 2]
105
+ # Act
106
+ result = weighted_random(mocked_process_step)
107
+
108
+ mocked_choices.assert_called_once_with(mock_group_users, weights=mocker.ANY)
109
+ assignees, weights = (
110
+ mocked_choices.call_args.args[0],
111
+ mocked_choices.call_args.kwargs["weights"],
185
112
  )
186
- process_step_factory(
187
- state=ProcessStep.StepState.FINISHED,
188
- process=process_step.process,
189
- group=process_step.group,
190
- step=user_step,
113
+ # Assert
114
+ assert assignees == mock_group_users
115
+ assert weights == pytest.approx([2 / 3, 1 / 3], rel=1e-2)
116
+ assert result in mock_group_users
117
+
118
+ def test_weighted_random_no_group(self, mocker: MockerFixture, mocked_process_step):
119
+ # Arrange
120
+ mocked_process_step.group = None
121
+ mocked_failed_method = mocker.patch.object(mocked_process_step.step.get_casted_step(), "set_failed")
122
+ # Act
123
+ result = weighted_random(mocked_process_step)
124
+ # Assert
125
+ mocked_failed_method.assert_called_once()
126
+ assert result is None
127
+
128
+ def test_weighted_random_empty_group(self, mocker: MockerFixture, mocked_process_step, mocked_group):
129
+ # Arrange
130
+ mocked_group.user_set.count.return_value = 0
131
+ mocked_process_step.group = mocked_group
132
+ mocked_fail_method = mocker.patch.object(mocked_process_step.step.get_casted_step(), "set_failed")
133
+ # Act
134
+ result = weighted_random(mocked_process_step)
135
+ # Assert
136
+ mocked_fail_method.assert_called_once()
137
+ assert result is None
138
+
139
+ def test_weighted_random_no_prior_assignments(self, mocker: MockerFixture, mocked_process_step, mocked_group):
140
+ # Arrange
141
+ user_a = mocker.MagicMock(spec=User, pk=1)
142
+ user_b = mocker.MagicMock(spec=User, pk=2)
143
+
144
+ mock_group_users = [user_a, user_b]
145
+ mocked_group.user_set.all.return_value = mock_group_users = [user_a, user_b]
146
+ mocked_group.user_set.count.return_value = 2
147
+ mocked_process_step.group = mocked_group
148
+
149
+ mocked_choices = mocker.patch(
150
+ "wbcore.contrib.workflow.workflows.assignees.choices",
151
+ return_value=[user_b],
191
152
  )
192
- assert weighted_random(process_step)
193
- assert len(mock_choices.call_args.args) == 1
194
- assert set(mock_choices.call_args.args[0]) == {assignee1, assignee2}
195
- assert not mock_choices.call_args.kwargs
196
-
197
- @patch("wbcore.contrib.workflow.workflows.assignees.choices")
198
- def test_weighted_random_no_group(self, mock_choices, process_step_factory):
199
- process_step = process_step_factory()
200
- assert not weighted_random(process_step)
201
- assert not mock_choices.called
202
- assert process_step.state == ProcessStep.StepState.FAILED
203
-
204
- @patch("wbcore.contrib.workflow.workflows.assignees.choices")
205
- def test_weighted_random_no_group_members(self, mock_choices, process_step_factory):
206
- process_step = process_step_factory(group=GroupFactory())
207
- assert not weighted_random(process_step)
208
- assert not mock_choices.called
209
- assert process_step.state == ProcessStep.StepState.FAILED
210
-
211
- def test_random_group_member(self, process_step_factory):
212
- group = GroupFactory()
213
- process_step = process_step_factory(group=group)
214
- UserFactory(groups=[group])
215
- UserFactory(groups=[group])
216
- assert random_group_member(process_step)
217
-
218
- def test_random_group_member_no_group(self, process_step_factory):
219
- process_step = process_step_factory()
220
- assert not random_group_member(process_step)
221
- assert process_step.state == ProcessStep.StepState.FAILED
222
-
223
- def test_random_group_member_no_group_members(self, process_step_factory):
224
- process_step = process_step_factory(group=GroupFactory())
225
- assert not random_group_member(process_step)
226
- assert process_step.state == ProcessStep.StepState.FAILED
153
+ mocked_qs = mocker.patch("wbcore.contrib.workflow.workflows.assignees.ProcessStep.objects.filter")
154
+ mocked_query = mocked_qs.return_value
155
+ mocked_query.exclude.return_value.values_list.return_value = []
156
+ # Act
157
+ result = weighted_random(mocked_process_step)
158
+ # Assert
159
+ mocked_choices.assert_called_once_with(mock_group_users)
160
+ assert result == user_b
161
+
162
+ def test_weighted_random_single_user(self, mocker: MockerFixture, mocked_process_step, mocked_group):
163
+ # Arrange
164
+ user_a = mocker.MagicMock(spec=User, pk=1)
165
+ mocked_group.user_set.all.return_value = [user_a]
166
+ mocked_group.user_set.count.return_value = 1
167
+ mocked_process_step.group = mocked_group
168
+ mocked_process_step.process = mocker.MagicMock(spec=Process)
169
+
170
+ mocked_qs = mocker.patch("wbcore.contrib.workflow.workflows.assignees.ProcessStep.objects.filter")
171
+
172
+ mocked_query = mocked_qs.return_value
173
+ mocked_query.exclude.return_value.values_list.return_value = []
174
+ # Act
175
+ result = weighted_random(mocked_process_step)
176
+ # Assert
177
+ assert result == user_a
178
+
179
+ def test_random_group_member_multiple_users(self, mocker: MockerFixture, mocked_process_step, mocked_group):
180
+ # Arrange
181
+ mocked_process_step = mocker.MagicMock(spec=ProcessStep)
182
+
183
+ user_a = mocker.MagicMock(spec=User)
184
+ user_b = mocker.MagicMock(spec=User)
185
+ user_c = mocker.MagicMock(spec=User)
186
+ users = [user_a, user_b, user_c]
187
+
188
+ mocked_group.user_set.exists.return_value = True
189
+ mocked_group.user_set.count.return_value = 3
190
+ mocked_group.user_set.all.return_value = users
191
+
192
+ mocked_process_step.group = mocked_group
193
+
194
+ mock_randint = mocker.patch("wbcore.contrib.workflow.workflows.assignees.randint", return_value=1)
195
+ # Act
196
+ result = random_group_member(mocked_process_step)
197
+ # Assert
198
+ mock_randint.assert_called_once_with(0, 2)
199
+ assert result == user_b # index 1
200
+
201
+ def test_random_group_member_single_user(self, mocker: MockerFixture, mocked_process_step, mocked_group):
202
+ # Arrange
203
+ mocked_process_step = mocker.MagicMock(spec=ProcessStep)
204
+
205
+ user_a = mocker.MagicMock(spec=User)
206
+ mocked_group.user_set.exists.return_value = True
207
+ mocked_group.user_set.count.return_value = 1
208
+ mocked_group.user_set.all.return_value = [user_a]
209
+
210
+ mocked_process_step.group = mocked_group
211
+ # Act
212
+ result = random_group_member(mocked_process_step)
213
+ # Assert
214
+ assert result == user_a
215
+
216
+ def test_random_group_member_empty_group(self, mocker, mocked_process_step, mocked_group):
217
+ # Arrange
218
+ mocked_process_step.group = mocked_group
219
+ mocked_process_step.group.user_set.exists.return_value = False
220
+
221
+ mocked_set_failed = mocker.patch.object(mocked_process_step.step.get_casted_step(), "set_failed")
222
+ # Act
223
+ result = random_group_member(mocked_process_step)
224
+ # Assert
225
+ mocked_set_failed.assert_called_once()
226
+ assert result is None
227
+
228
+ def test_random_group_member_no_group(self, mocker, mocked_process_step):
229
+ # Arrange
230
+ mocked_process_step.group = None
231
+ mocked_set_failed = mocker.patch.object(mocked_process_step.step.get_casted_step(), "set_failed")
232
+ # Act
233
+ result = random_group_member(mocked_process_step)
234
+ # Assert
235
+ mocked_set_failed.assert_called_once()
236
+ assert result is None
@@ -164,7 +164,10 @@ class AssignedProcessStepModelViewSet(ProcessStepModelViewSet):
164
164
  )
165
165
  )
166
166
  )
167
- .annotate(attached_model=F("step__workflow__model__model"), workflow_name=F("step__workflow__name"))
167
+ .annotate(
168
+ attached_model=F("step__workflow__model__model"),
169
+ workflow_name=F("step__workflow__name"),
170
+ )
168
171
  ).select_related("step__workflow")
169
172
  return qs
170
173
 
@@ -38,7 +38,10 @@ def weighted_random(process_step: ProcessStep, **kwargs) -> User | None:
38
38
  if (group := process_step.group) and (group_user_count := group.user_set.count()):
39
39
  similar_process_steps_selected_assignees: list[str] = list(
40
40
  ProcessStep.objects.filter(
41
- state__in=[ProcessStep.StepState.FINISHED, ProcessStep.StepState.ACTIVE],
41
+ state__in=[
42
+ ProcessStep.StepState.FINISHED,
43
+ ProcessStep.StepState.ACTIVE,
44
+ ],
42
45
  process=process_step.process,
43
46
  group=group,
44
47
  assignee__isnull=False,
@@ -56,18 +59,19 @@ def weighted_random(process_step: ProcessStep, **kwargs) -> User | None:
56
59
  # We redistribute each occurrence number between all of the other list items to increase their probability
57
60
  redistributed_list: list[int] = [0 for i in range(group_user_count)]
58
61
  for index, elem in enumerate(number_of_past_assignee_occurrences):
59
- for index2, elem2 in enumerate(redistributed_list):
62
+ for index2 in range(len(redistributed_list)):
60
63
  if not index2 == index:
61
64
  redistributed_list[index2] += elem / (group_user_count - 1) if elem else 0
62
65
  # Transform the list of absolute values into percentages
63
66
  new_weights: list[float] = [x / sum(redistributed_list) for x in redistributed_list]
64
- new_assignee: User = choices(group_member_list, weights=new_weights)[0]
67
+ new_assignee: User = choices(group_member_list, weights=new_weights)[0] # noqa
65
68
  else:
66
- new_assignee: User = choices(group_member_list)[0]
69
+ new_assignee: User = choices(group_member_list)[0] # noqa
67
70
  return new_assignee
68
71
 
69
72
  process_step.step.get_casted_step().set_failed(
70
- process_step, _("Error in assignee method: No populated group to pick assignee from selected!")
73
+ process_step,
74
+ _("Error in assignee method: No populated group to pick assignee from selected!"),
71
75
  )
72
76
  return None
73
77
 
@@ -75,9 +79,10 @@ def weighted_random(process_step: ProcessStep, **kwargs) -> User | None:
75
79
  @register_assignee("Random Group Member")
76
80
  def random_group_member(process_step: ProcessStep, **kwargs) -> User | None:
77
81
  if (group := process_step.group) and group.user_set.exists():
78
- return group.user_set.all()[randint(0, group.user_set.count() - 1)]
82
+ return group.user_set.all()[randint(0, group.user_set.count() - 1)] # noqa
79
83
 
80
84
  process_step.step.get_casted_step().set_failed(
81
- process_step, _("Error in assignee method: No populated group to pick assignee from selected!")
85
+ process_step,
86
+ _("Error in assignee method: No populated group to pick assignee from selected!"),
82
87
  )
83
88
  return None
@@ -2,6 +2,10 @@ from django.utils.translation import gettext as _
2
2
  from dynamic_preferences.preferences import Section
3
3
  from dynamic_preferences.registries import global_preferences_registry
4
4
  from dynamic_preferences.types import IntegerPreference, StringPreference
5
+ from dynamic_preferences.users.registries import user_preferences_registry
6
+
7
+ from wbcore.contrib.dynamic_preferences.types import ChoicePreference, LanguageChoicePreference
8
+ from wbcore.utils.date import get_timezone_choices
5
9
 
6
10
  wbcore = Section("wbcore")
7
11
 
@@ -27,3 +31,101 @@ class SystemUserEmailPeriod(StringPreference):
27
31
 
28
32
  verbose_name = _("System User Email")
29
33
  help_text = _("System User Email")
34
+
35
+
36
+ @user_preferences_registry.register
37
+ class LanguagePreference(LanguageChoicePreference):
38
+ weight = -1
39
+ choices = [
40
+ ("de", "Deutsch"),
41
+ ("fr", "Français"),
42
+ ("en", "English"),
43
+ ]
44
+
45
+ section = wbcore
46
+ name = "language"
47
+ default = "en"
48
+
49
+ verbose_name = _("System Language")
50
+ help_text = _("Select the language you want the Workbench to display.")
51
+
52
+
53
+ @user_preferences_registry.register
54
+ class TimezonePreference(ChoicePreference):
55
+ weight = 0
56
+ # Value is a IANA timezone name
57
+ choices = get_timezone_choices()
58
+ section = wbcore
59
+ name = "timezone"
60
+ default = "Europe/Berlin"
61
+
62
+ verbose_name = _("Timezone")
63
+ help_text = _("Pick the timezone in which you want the workbench's dates to be displayed in.")
64
+
65
+
66
+ @user_preferences_registry.register
67
+ class DateFormatPreference(ChoicePreference):
68
+ weight = 1
69
+ choices = [
70
+ ("DD.MM.YYYY", "13.04.2007"),
71
+ ("DD/MM/YYYY", "13/04/2007"),
72
+ ("DD-MM-YYYY", "13-04-2007"),
73
+ ("MM/DD/YYYY", "04/13/2007"),
74
+ ("MM-DD-YYYY", "04-13-2007"),
75
+ ("YYYY/MM/DD", "2007/04/13"),
76
+ ("YYYY-MM-DD", "2007-04-13"),
77
+ ("MMM DD, YYYY", "April 13, 2007"),
78
+ ("DD MMM YYYY", "13 April 2007"),
79
+ ("dddd, DD MMM YYYY", "Friday, 13 April 2007"),
80
+ ("ddd, DD MMM YYYY", "Fri, 13 April 2007"),
81
+ ("ddd Do MMMM YYYY", "Fri 13th April 2007"),
82
+ ]
83
+
84
+ section = wbcore
85
+ name = "date_format"
86
+ default = "YYYY-MM-DD"
87
+
88
+ verbose_name = _("Date Format")
89
+ help_text = _("Choose how you want dates to appear throughout the Workbench.")
90
+
91
+
92
+ @user_preferences_registry.register
93
+ class TimeFormatPreference(ChoicePreference):
94
+ weight = 2
95
+ choices = [
96
+ ("HH:mm", "14:05"),
97
+ ("hh:mm", "02:05"),
98
+ ("h:mm", "2:05"),
99
+ ("HH [h] mm", "14 h 05"),
100
+ ("HH[h]mm", "14h05"),
101
+ ("h:mm A", "2:05 PM"),
102
+ ("h:mm a", "2:05 pm"),
103
+ ("hh:mm A", "02:05 PM"),
104
+ ("hh:mm a", "02:05 pm"),
105
+ ]
106
+
107
+ section = wbcore
108
+ name = "time_format"
109
+ default = "HH:mm"
110
+
111
+ verbose_name = _("Time Format")
112
+ help_text = _("Choose how you want times to appear throughout the Workbench.")
113
+
114
+
115
+ @user_preferences_registry.register
116
+ class NumberFormatPreference(ChoicePreference):
117
+ weight = 3
118
+ # Value is a BCP 47 region subtag
119
+ choices = [
120
+ ("US", "1,234,567.89"),
121
+ ("FR", "1\u202f234\u202f567,89"),
122
+ ("DE", "1.234.567,89"),
123
+ ("CH", "1’234’567.89"),
124
+ ]
125
+
126
+ section = wbcore
127
+ name = "number_format"
128
+ default = "US"
129
+
130
+ verbose_name = _("Number Format")
131
+ help_text = _("Choose how you want numbers to appear throughout the Workbench.")