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/test/tests.py CHANGED
@@ -1,7 +1,6 @@
1
1
  import pytest
2
2
  from django.conf import settings
3
3
  from django.db import models
4
- from django.urls import get_resolver
5
4
 
6
5
  from wbcore import serializers, viewsets
7
6
  from wbcore.pandas.views import PandasAPIViewSet
@@ -24,8 +23,6 @@ This is useful mainly when running tests, or running locally without Celery work
24
23
  """
25
24
  settings.CELERY_TASK_ALWAYS_EAGER = True
26
25
 
27
- get_resolver().url_patterns
28
-
29
26
 
30
27
  def modules_condition(module):
31
28
  return not module.startswith(("wbcore", "django", "rest_framework", "dynamic_preferences", "eventtools")) or (
@@ -122,11 +119,11 @@ class GenerateTest:
122
119
  def _test_chartviewsets(_self, cvs):
123
120
  self.test_chartviewsets(cvs)
124
121
 
125
- setattr(test_class, "test_models", _test_models)
126
- setattr(test_class, "test_serializers", _test_serializers)
127
- setattr(test_class, "test_representationviewsets", _test_representationviewsets)
128
- setattr(test_class, "test_modelviewsets", _test_modelviewsets)
129
- setattr(test_class, "test_pandasviews", _test_pandasviews)
130
- setattr(test_class, "test_chartviewsets", _test_chartviewsets)
122
+ test_class.test_models = _test_models
123
+ test_class.test_serializers = _test_serializers
124
+ test_class.test_representationviewsets = _test_representationviewsets
125
+ test_class.test_modelviewsets = _test_modelviewsets
126
+ test_class.test_pandasviews = _test_pandasviews
127
+ test_class.test_chartviewsets = _test_chartviewsets
131
128
 
132
129
  return test_class
wbcore/test/utils.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import datetime
2
2
  import json
3
+ from contextlib import suppress
3
4
  from functools import partial
4
5
  from typing import Any, Dict
5
6
 
@@ -44,7 +45,7 @@ def get_model_factory(model):
44
45
  return mfs[0]
45
46
 
46
47
 
47
- def get_data_from_factory(instance, viewset, delete=False, update=False, superuser=None, factory=None):
48
+ def get_data_from_factory(instance, viewset, delete=False, update=False, superuser=None, factory=None): # noqa: C901
48
49
  """
49
50
  Our goal here is to get the serializer dynamically based on the viewset, use this serializer to generate data for the post and update test.
50
51
  """
@@ -113,14 +114,12 @@ def get_data_from_factory(instance, viewset, delete=False, update=False, superus
113
114
  and _field.many_to_one
114
115
  and (_related_fields := _field.related_fields)
115
116
  ):
116
- try:
117
+ with suppress(Exception):
117
118
  lh_field, rh_field = _related_fields[0]
118
119
  if isinstance(lh_field, models.fields.AutoField) or isinstance(
119
120
  rh_field, models.fields.AutoField
120
121
  ):
121
122
  data[key] = get_model_factory(dict_fields_models[key].related_model).id
122
- except Exception:
123
- pass
124
123
  if update or delete:
125
124
  _kwargs = {"user": superuser, "obj_factory": obj_factory}
126
125
  if delete:
@@ -11,14 +11,14 @@ USER_PASSWORD = "User_Password"
11
11
  @pytest.mark.django_db
12
12
  class TestE2ELogin:
13
13
  def test_login_success(self, live_server, selenium):
14
- user: User = SuperUserFactory(plaintext_password=USER_PASSWORD)
14
+ user: User = SuperUserFactory(plaintext_password=USER_PASSWORD) # noqa
15
15
  selenium.get(live_server.url)
16
16
  assert not find_element(selenium, "//div[contains(@class, 'sidebar')]")
17
17
  login(selenium, user.email, USER_PASSWORD)
18
18
  assert find_element(selenium, "//div[contains(@class, 'sidebar')]")
19
19
 
20
20
  def test_login_failure(self, live_server, selenium):
21
- user: User = SuperUserFactory(plaintext_password=USER_PASSWORD)
21
+ user: User = SuperUserFactory(plaintext_password=USER_PASSWORD) # noqa
22
22
  selenium.get(live_server.url)
23
23
  assert not find_element(selenium, "//p[@type='error']")
24
24
  login(selenium, user.email + "Wrong Name", USER_PASSWORD)
@@ -1,15 +1,12 @@
1
1
  import pytest
2
- from faker import Faker
3
2
 
4
3
  from wbcore.cache.decorators import cache_table
5
4
  from wbcore.cache.registry import periodic_cache_registry
6
5
 
7
- fake = Faker()
8
-
9
6
 
10
7
  @pytest.mark.parametrize(
11
8
  "timeout, key_prefix, periodic_caching_view_kwargs, periodic_caching_get_parameters",
12
- [(fake.pyint(), fake.word(), [{"a": "a"}], [{"b": "b"}])],
9
+ [(2, "Foo", [{"a": "a"}], [{"b": "b"}])],
13
10
  )
14
11
  def test_cache_table(timeout, key_prefix, periodic_caching_view_kwargs, periodic_caching_get_parameters):
15
12
  @cache_table(
@@ -22,9 +19,9 @@ def test_cache_table(timeout, key_prefix, periodic_caching_view_kwargs, periodic
22
19
  class CacheTable:
23
20
  pass
24
21
 
25
- assert getattr(CacheTable, "CACHE_ENABLED") is True
26
- assert getattr(CacheTable, "CACHE_TIMEOUT") == timeout
27
- assert getattr(CacheTable, "CACHE_KEY_PREFIX") == key_prefix
22
+ assert CacheTable.CACHE_ENABLED is True
23
+ assert CacheTable.CACHE_TIMEOUT == timeout
24
+ assert CacheTable.CACHE_KEY_PREFIX == key_prefix
28
25
  cache_entry = periodic_cache_registry.classes[0]
29
26
  assert cache_entry.view_class == CacheTable
30
27
  assert cache_entry.view_kwargs == periodic_caching_view_kwargs
@@ -1,13 +1,10 @@
1
1
  import pytest
2
2
  from django.conf import settings
3
- from faker import Faker
4
3
 
5
4
  from wbcore.configs.decorators import register_config
6
5
  from wbcore.configs.registry import ConfigRegistry
7
6
  from wbcore.configs.views import ConfigAPIView
8
7
 
9
- fake = Faker()
10
-
11
8
 
12
9
  def test_registry(config_registry: ConfigRegistry):
13
10
  configs = config_registry.get_config_dict()
@@ -40,7 +37,7 @@ def test_menu_calendar_config(config_registry: ConfigRegistry):
40
37
  assert menu_calendar
41
38
 
42
39
 
43
- @pytest.mark.parametrize("text, version", [(fake.sentence(), fake.word())])
40
+ @pytest.mark.parametrize("text, version", [("Foo bar", "Foo")])
44
41
  def test_beta_button_config(config_registry: ConfigRegistry, text, version):
45
42
  settings.BETA_BUTTON_VERSION = version
46
43
  settings.BETA_BUTTON_TEXT = text
@@ -55,7 +52,7 @@ def test_config_registry_decorator():
55
52
 
56
53
  assert not hasattr(some_callable, "_is_config")
57
54
  some_callable = register_config(some_callable)
58
- assert getattr(some_callable, "_is_config")
55
+ assert some_callable._is_config
59
56
 
60
57
 
61
58
  def test_config_view(rf):
@@ -1,6 +1,7 @@
1
1
  import pytest
2
2
 
3
- from wbcore.enums import Button, Unit
3
+ from wbcore.enums import Unit
4
+ from wbcore.metadata.configs.buttons.enums import Button
4
5
 
5
6
 
6
7
  class TestUnit:
@@ -33,7 +33,7 @@ class TestChoiceField:
33
33
  assert self.field.field_type == WBCoreType.SELECT.value
34
34
 
35
35
  def test_representation(self):
36
- choices = [{"label": choice[1], "value": choice[0]} for choice in self.CHOICES]
36
+ choices = [{"label": choice[1], "value": choice[0], "group": None} for choice in self.CHOICES]
37
37
  assert self.field.get_representation(None, "field_name") == (
38
38
  "field_name",
39
39
  {
@@ -47,3 +47,11 @@ class TestChoiceField:
47
47
  "depends_on": [],
48
48
  },
49
49
  )
50
+
51
+ def test_representation_with_group(self):
52
+ field = ChoiceField(choices=self.CHOICES, group_key_mapping={"choice1": "group1"})
53
+ rep = field.get_representation(None, "field_name")[1]
54
+ assert rep["choices"] == [
55
+ {"label": "Choice 1", "value": "choice1", "group": "group1"},
56
+ {"label": "Choice 2", "value": "choice2", "group": None},
57
+ ]
@@ -1,15 +1,12 @@
1
1
  from decimal import Decimal
2
2
 
3
3
  import pytest
4
- from faker import Faker
5
4
  from rest_framework.exceptions import ValidationError
6
5
 
7
6
  from wbcore.serializers import DecimalField, FloatField, IntegerField
8
7
  from wbcore.serializers.fields import YearField
9
8
  from wbcore.serializers.fields.types import WBCoreType
10
9
 
11
- fake = Faker()
12
-
13
10
 
14
11
  class TestIntegerField:
15
12
  def setup_method(self):
@@ -44,9 +41,8 @@ class TestIntegerField:
44
41
  "max_digits": 34,
45
42
  "decorators": [],
46
43
  "depends_on": [],
47
- "decimal_mark": ".",
48
- "delimiter": ",",
49
44
  "signed": True,
45
+ "disable_formatting": False,
50
46
  },
51
47
  )
52
48
 
@@ -87,9 +83,8 @@ class TestDecimalField:
87
83
  "decorators": [],
88
84
  "depends_on": [],
89
85
  "display_mode": "decimal",
90
- "decimal_mark": ".",
91
- "delimiter": ",",
92
86
  "signed": True,
87
+ "disable_formatting": False,
93
88
  },
94
89
  )
95
90
 
@@ -109,9 +104,8 @@ class TestDecimalField:
109
104
  "decorators": [],
110
105
  "depends_on": [],
111
106
  "display_mode": "decimal",
112
- "decimal_mark": ".",
113
- "delimiter": ",",
114
107
  "signed": True,
108
+ "disable_formatting": False,
115
109
  },
116
110
  )
117
111
 
@@ -149,9 +143,8 @@ class TestFloatField:
149
143
  "decorators": [],
150
144
  "depends_on": [],
151
145
  "display_mode": "decimal",
152
- "decimal_mark": ".",
153
- "delimiter": ",",
154
146
  "signed": True,
147
+ "disable_formatting": False,
155
148
  },
156
149
  )
157
150
 
@@ -163,12 +156,11 @@ class TestYearField:
163
156
  "label",
164
157
  ),
165
158
  [
166
- (fake.word(), fake.word()),
159
+ ("Foo", "Bar"),
167
160
  ],
168
161
  )
169
162
  def test_year_field_values(self, key, label):
170
- field = YearField(label=label, precision=2, decimal_mark="*", delimiter=",")
163
+ field = YearField(label=label, precision=2)
171
164
  representation = field.get_representation(None, key)[1]
172
165
  assert representation["precision"] == 0
173
- assert representation["decimal_mark"] == "."
174
- assert representation["delimiter"] == ""
166
+ assert representation["disable_formatting"]
@@ -25,9 +25,8 @@ class TestStarRatingField:
25
25
  "decorators": [],
26
26
  "depends_on": [],
27
27
  "display_mode": "decimal",
28
- "decimal_mark": "",
29
- "delimiter": "",
30
28
  "signed": True,
29
+ "disable_formatting": True,
31
30
  },
32
31
  )
33
32
 
@@ -14,47 +14,47 @@ class TestWBCoreFilterMixin:
14
14
  def view(self):
15
15
  return ViewSet()
16
16
 
17
- @pytest.mark.parametrize("field_name", [fake.word()])
17
+ @pytest.mark.parametrize("field_name", ["Foo"])
18
18
  def test_key_column_name(self, field_name):
19
19
  filter_field = CharFilter(column_field_name=field_name, field_name=field_name)
20
20
  assert filter_field.key == field_name
21
21
 
22
- @pytest.mark.parametrize("field_name", [fake.word()])
22
+ @pytest.mark.parametrize("field_name", ["Foo"])
23
23
  def test_key_field_name(self, field_name):
24
24
  filter_field = CharFilter(field_name=field_name)
25
25
  assert filter_field.key == field_name
26
26
 
27
- @pytest.mark.parametrize("label", [fake.word()])
27
+ @pytest.mark.parametrize("label", ["Foo"])
28
28
  def test_get_label_with_label(self, label):
29
29
  filter_field = CharFilter(label=label)
30
30
  assert filter_field.get_label() == label
31
31
 
32
- @pytest.mark.parametrize("field_name1, field_name2", [(fake.word(), fake.word())])
32
+ @pytest.mark.parametrize("field_name1, field_name2", [("Foo", "Bar")])
33
33
  def test_get_label_without_label(self, field_name1, field_name2):
34
34
  field_name = field_name1 + "_" + field_name2
35
35
  filter_field = CharFilter(field_name=field_name)
36
36
  assert filter_field.get_label() == field_name1.title() + " " + field_name2.title()
37
37
 
38
- @pytest.mark.parametrize("default", [fake.word()])
39
- def test__get_default_with_callable(self, rf, default, view):
40
- filter_field = CharFilter(default=lambda f, r, v: default)
41
- assert filter_field._get_default(rf, view) == default
38
+ @pytest.mark.parametrize("initial", ["Foo"])
39
+ def test__get_initial_with_callable(self, rf, initial, view):
40
+ filter_field = CharFilter(initial=lambda f, r, v: initial)
41
+ assert filter_field._get_initial(rf, view) == initial
42
42
 
43
- @pytest.mark.parametrize("default", [fake.word()])
44
- def test__get_default_with_callable_str(self, rf, default, view):
43
+ @pytest.mark.parametrize("initial", ["Foo"])
44
+ def test__get_initial_with_callable_str(self, rf, initial, view):
45
45
  class CustomFilterField(CharFilter):
46
- def custom_default(self, *args):
47
- return default
46
+ def custom_initial(self, *args):
47
+ return initial
48
48
 
49
- filter_field = CustomFilterField(default="custom_default")
50
- assert filter_field._get_default(rf, view) == default
49
+ filter_field = CustomFilterField(initial="custom_initial")
50
+ assert filter_field._get_initial(rf, view) == initial
51
51
 
52
- @pytest.mark.parametrize("default", [fake.word()])
53
- def test__get_default_with_default(self, rf, default, view):
54
- filter_field = CharFilter(default=default)
55
- assert filter_field._get_default(rf, view) == default
52
+ @pytest.mark.parametrize("initial", ["Foo"])
53
+ def test__get_initial_with_initial(self, rf, initial, view):
54
+ filter_field = CharFilter(initial=initial)
55
+ assert filter_field._get_initial(rf, view) == initial
56
56
 
57
- @pytest.mark.parametrize("name", [fake.word()])
57
+ @pytest.mark.parametrize("name", ["Foo"])
58
58
  def test_get_representation(self, name, rf, view):
59
59
  filter_field = CharFilter(field_name=name)
60
60
  request = rf.get("")
@@ -62,48 +62,48 @@ class TestWBCoreFilterMixin:
62
62
  for key in ["label", "key", "label_format"]:
63
63
  assert key in rep
64
64
 
65
- @pytest.mark.parametrize("name, initial", [(fake.word(), fake.word())])
65
+ @pytest.mark.parametrize("name, initial", [("Foo", "Bar")])
66
66
  def test_get_representation_request_initial(self, name, initial, rf, view):
67
67
  filter_field = CharFilter(field_name=name)
68
68
  rf.GET = {name: initial}
69
69
  _, le = filter_field.get_representation(rf, name, view)
70
70
  assert le["input_properties"]["initial"] == initial
71
71
 
72
- @pytest.mark.parametrize("name, default", [(fake.word(), fake.word())])
73
- def test_get_representation_field_default(self, name, default, rf, view):
74
- filter_field = CharFilter(field_name=name, default=default)
72
+ @pytest.mark.parametrize("name, initial", [("Foo", "Bar")])
73
+ def test_get_representation_field_initial(self, name, initial, rf, view):
74
+ filter_field = CharFilter(field_name=name, initial=initial)
75
75
  request = rf.get("")
76
76
  _, le = filter_field.get_representation(request, name, view)
77
- assert le["input_properties"]["default"] == default
77
+ assert le["input_properties"]["initial"] == initial
78
78
 
79
- @pytest.mark.parametrize("name, default, help_text", [(fake.word(), fake.word(), fake.word())])
80
- def test_get_help_text(self, name, default, help_text, rf, view):
79
+ @pytest.mark.parametrize("name, initial, help_text", [("Foo", "Bar", "Sesquipedalophobie")])
80
+ def test_get_help_text(self, name, initial, help_text, rf, view):
81
81
  request = rf.get("")
82
82
 
83
83
  # assert that no field nor help text nor label returns an empty help_text
84
- filter_field = CharFilter(field_name=name, default=default, help_text=None)
84
+ filter_field = CharFilter(field_name=name, initial=initial, help_text=None)
85
85
  assert filter_field.get_representation(request, name, view)[0]["help_text"] is None
86
86
 
87
87
  # assert that no field nor help text but a label returns an "Filter by {{label}}"
88
- filter_field = CharFilter(field_name=name, default=default, help_text=None, label=help_text)
88
+ filter_field = CharFilter(field_name=name, initial=initial, help_text=None, label=help_text)
89
89
  assert filter_field.get_representation(request, name, view)[0]["help_text"] == f"Filter by {help_text}"
90
90
 
91
- filter_field = CharFilter(field_name=name, default=default, help_text=help_text)
91
+ filter_field = CharFilter(field_name=name, initial=initial, help_text=help_text)
92
92
  assert filter_field.get_representation(request, name, view)[0]["help_text"] == help_text
93
93
 
94
94
  @patch("wbcore.filters.mixins.get_model_field")
95
- @pytest.mark.parametrize("name, default, text", [(fake.word(), fake.word(), fake.word())])
96
- def test_get_help_text_from_model_help_text(self, mock_fct, name, default, text, rf, view):
95
+ @pytest.mark.parametrize("name, initial, text", [("Foo", "Bar", "Sesquipedalophobie")])
96
+ def test_get_help_text_from_model_help_text(self, mock_fct, name, initial, text, rf, view):
97
97
  class Field:
98
98
  help_text = text
99
99
 
100
- class dummy_parent:
101
- class _meta:
100
+ class DummyParent:
101
+ class _meta: # noqa
102
102
  model = "tmp"
103
103
 
104
104
  mock_fct.return_value = Field
105
105
 
106
- filter_field = CharFilter(field_name=name, default=default, help_text=None)
107
- filter_field.parent = dummy_parent()
106
+ filter_field = CharFilter(field_name=name, initial=initial, help_text=None)
107
+ filter_field.parent = DummyParent()
108
108
  request = rf.get("")
109
109
  assert filter_field.get_representation(request, name, view)[0]["help_text"] == text
@@ -10,7 +10,6 @@ def test_tree_group_open_level_list_display():
10
10
  fields=[],
11
11
  tree=True,
12
12
  tree_group_field="field",
13
- tree_group_label="label",
14
13
  tree_group_open_level=3,
15
14
  tree_group_level_options=[TreeGroupLevelOption(list_endpoint="endpoint")],
16
15
  )
@@ -24,7 +23,6 @@ def test_not_tree_group_open_level_list_display():
24
23
  fields=[],
25
24
  tree=False,
26
25
  tree_group_field="field",
27
- tree_group_label="label",
28
26
  tree_group_open_level=3,
29
27
  )
30
28
  )
@@ -3,7 +3,7 @@ from django.db import models
3
3
  from wbcore.models.orderable import OrderableModel
4
4
 
5
5
 
6
- class ForeignKeyClass(models.Model):
6
+ class ForeignKeyClass(models.Model): # noqa
7
7
  @classmethod
8
8
  def get_endpoint_basename(cls) -> str:
9
9
  return "wbcore:foreignkeyclass"
@@ -4,7 +4,7 @@ import pytest
4
4
  from rest_framework.test import APIRequestFactory
5
5
 
6
6
  from wbcore.utils.date import get_date_interval_from_request, shortcut
7
- from wbcore.utils.date_builder import *
7
+ from wbcore.utils.date_builder import Day, Now, WeekStart
8
8
 
9
9
 
10
10
  class TestGetDateInterval:
@@ -3,7 +3,31 @@ from operator import add, sub
3
3
 
4
4
  import pytest
5
5
 
6
- from wbcore.utils.date_builder import *
6
+ from wbcore.utils.date_builder import (
7
+ BusinessDay,
8
+ Day,
9
+ Hour,
10
+ HourEnd,
11
+ HourStart,
12
+ Minute,
13
+ MinuteEnd,
14
+ MinuteStart,
15
+ Month,
16
+ MonthEnd,
17
+ MonthStart,
18
+ Quarter,
19
+ QuarterEnd,
20
+ QuarterStart,
21
+ Second,
22
+ SecondEnd,
23
+ SecondStart,
24
+ Week,
25
+ WeekEnd,
26
+ WeekStart,
27
+ Year,
28
+ YearEnd,
29
+ YearStart,
30
+ )
7
31
 
8
32
 
9
33
  class TestDateBuilder:
@@ -36,7 +36,7 @@ class TestPrimaryMixin:
36
36
  instance_other
37
37
  } # Test the queryset are all instance related to pivot "other"
38
38
 
39
- @pytest.mark.parametrize("field", fake.word())
39
+ @pytest.mark.parametrize("field", ("Foo Bar"))
40
40
  def test_saving_primary(self, field):
41
41
  initial_instance = TestPrimaryModel.objects.create(primary=False, pivot="main", field=field)
42
42
  assert (
wbcore/urls.py CHANGED
@@ -1,6 +1,8 @@
1
1
  from django.urls import include, path
2
+ from dynamic_preferences.api.viewsets import GlobalPreferencesViewSet
2
3
 
3
4
  from wbcore.cache.views import clear_cache
5
+ from wbcore.contrib.dynamic_preferences.viewsets import UserPreferencesViewSet
4
6
  from wbcore.shares.views import ShareAPIView
5
7
 
6
8
  from .configs.views import ConfigAPIView
@@ -35,6 +37,8 @@ router.register(
35
37
  FrontendUserConfigurationModelViewSet,
36
38
  basename="frontenduserconfiguration",
37
39
  )
40
+ router.register(r"global_preferences", GlobalPreferencesViewSet, basename="global_preferences")
41
+ router.register(r"user_preferences", UserPreferencesViewSet, basename="user_preferences")
38
42
 
39
43
  router.register(r"version", VersionModelViewSet, basename="version")
40
44
  router.register(r"revision", RevisionModelViewSet, basename="revision")
wbcore/utils/date.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from collections import defaultdict
2
- from datetime import date, datetime, time, timedelta
2
+ from datetime import date, datetime, time, timedelta, timezone
3
+ from zoneinfo import ZoneInfo, available_timezones
3
4
 
4
5
  from dateutil import rrule
5
6
  from django.utils.dateparse import parse_date
@@ -153,8 +154,11 @@ def get_date_interval_from_request(
153
154
 
154
155
 
155
156
  def get_number_of_hours_between_dates(
156
- d1, d2, skip_weekends=True, list_public_holidays=False, hours_range=range(0, 23), granularity=12
157
+ d1, d2, skip_weekends=True, list_public_holidays=False, hours_range=None, granularity=12
157
158
  ):
159
+ if hours_range is None:
160
+ hours_range = range(0, 23)
161
+
158
162
  def convert_days_from_hours(hours, granularity, hours_per_day):
159
163
  return int(hours / granularity) * granularity / hours_per_day
160
164
 
@@ -219,3 +223,15 @@ def get_next_day_timedelta(now: datetime | None = None) -> int:
219
223
  if not now:
220
224
  now = datetime.now()
221
225
  return (datetime.combine(now.date() + timedelta(days=1), time(0, 0, 0)) - now).seconds
226
+
227
+
228
+ def get_timezone_choices() -> list[tuple[str, str]]:
229
+ now_utc = datetime.now(timezone.utc)
230
+ tz_tuples = [] # a list of (timezone_name, timezone_name (UTC offset))
231
+ for tz_name in sorted(available_timezones()):
232
+ tz = ZoneInfo(tz_name)
233
+ now_in_tz = now_utc.astimezone(tz)
234
+ offset_str = now_in_tz.strftime("UTC%z") # gives UTC+HHMM
235
+ offset_str = offset_str[:-2] + ":" + offset_str[-2:]
236
+ tz_tuples.append((tz_name, f"{tz_name} ({offset_str})"))
237
+ return tz_tuples
wbcore/utils/figures.py CHANGED
@@ -183,7 +183,7 @@ def get_horizontal_barplot(
183
183
  df,
184
184
  x_label="weighting",
185
185
  y_label="aggregated_title",
186
- colors=["#B4DAFF"],
186
+ colors: tuple[str, ...] = ("#B4DAFF",),
187
187
  colors_label=None,
188
188
  drop_null_x_value: bool = True,
189
189
  ):
@@ -204,7 +204,7 @@ def get_horizontal_barplot(
204
204
  if colors_label:
205
205
  colors = df[colors_label]
206
206
  opacity = 1
207
- for i, label in enumerate(x_label):
207
+ for label in x_label:
208
208
  data.append(
209
209
  go.Bar(
210
210
  name=label,
wbcore/utils/models.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from contextlib import suppress
2
+ from typing import Self
2
3
 
3
4
  from django.core.exceptions import FieldDoesNotExist, FieldError
4
5
  from django.db import models
@@ -12,6 +13,7 @@ from wbcore.contrib.color.enums import WBColor
12
13
  from wbcore.contrib.color.fields import ColorField
13
14
  from wbcore.contrib.icons import WBIcon
14
15
  from wbcore.contrib.icons.models import IconField
16
+ from wbcore.signals import post_clone
15
17
  from wbcore.utils.enum import ChoiceEnum
16
18
 
17
19
 
@@ -105,7 +107,7 @@ class Status(ChoiceEnum):
105
107
  @classmethod
106
108
  def get_color_map(cls):
107
109
  colors = [WBColor.YELLOW_LIGHT.value, WBColor.RED_LIGHT.value, WBColor.GREEN_LIGHT.value]
108
- return [choice for choice in zip(cls, colors)]
110
+ return [choice for choice in zip(cls, colors, strict=False)]
109
111
 
110
112
 
111
113
  class PrimaryMixin(models.Model):
@@ -162,9 +164,8 @@ class PrimaryMixin(models.Model):
162
164
  next_primary = qs.first()
163
165
  next_primary.primary = True
164
166
  next_primary.save()
165
- self.primary = False
166
- self.save()
167
167
  if no_deletion:
168
+ self.primary = False
168
169
  for field_name in self.PRIMARY_ATTR_FIELDS:
169
170
  with suppress(FieldError, FieldDoesNotExist):
170
171
  if self._meta.get_field(field_name).null: # ensure the field is nullable
@@ -272,5 +273,21 @@ class ResolvableModelMixin(models.Model):
272
273
  resolved = models.BooleanField(default=False, verbose_name="Resolved")
273
274
 
274
275
  def delete(self, *args, **kwargs):
275
- assert not self.resolved, "Resolved Entities cannot be deleted."
276
+ if self.resolved:
277
+ raise ValueError("Resolved Entities cannot be deleted.")
276
278
  super().delete(*args, **kwargs)
279
+
280
+
281
+ class CloneMixin(models.Model):
282
+ # Clone mixin
283
+
284
+ def _clone(self, **kwargs) -> Self:
285
+ raise NotImplementedError()
286
+
287
+ def clone(self, **kwargs) -> Self:
288
+ clone = self._clone(**kwargs)
289
+ post_clone.send(sender=self.__class__, instance=self, clone=clone, **kwargs)
290
+ return clone
291
+
292
+ class Meta:
293
+ abstract = True
@@ -0,0 +1,7 @@
1
+ from reportlab.platypus import Paragraph as BaseParagraph
2
+
3
+
4
+ class FormattedParagraph(BaseParagraph):
5
+ def __init__(self, text, *args, **kwargs):
6
+ text = text.replace("<br>", "<br/>") # convert the HTML line break into a compatible XML line break tag
7
+ super().__init__(text, *args, **kwargs)
wbcore/utils/rrules.py CHANGED
@@ -21,6 +21,8 @@ def convert_rrulestr_to_dict(
21
21
  rule_dict["wkst"] = getattr(rrule, wkst)
22
22
  if byday := rule_dict.pop("byday", None):
23
23
  rule_dict["byweekday"] = [getattr(rrule, day.strip()) for day in byday.split(",")]
24
+ if bymonthday := rule_dict.pop("bymonthday", None):
25
+ rule_dict["bymonthday"] = [int(v) for v in bymonthday.split(",")]
24
26
  if dtstart:
25
27
  rule_dict["dtstart"] = dtstart
26
28
  if count:
@@ -46,7 +48,7 @@ def humanize_rrule(rrule_str: rrule) -> str:
46
48
  Returns:
47
49
  A humanized version of the rrule
48
50
  """
49
- if not rrule_str._freq:
51
+ if rrule_str._freq is None:
50
52
  raise ValueError("We do no support humanization of rrule without frequency yet")
51
53
  text = "Every "
52
54
  freq = rrule.FREQNAMES[rrule_str._freq]
@@ -21,7 +21,7 @@ class StringSourceLoader(importlib.abc.SourceLoader):
21
21
 
22
22
  def exec_module(self, module):
23
23
  code = self.source_to_code(self.data, self.get_filename(module.__name__))
24
- exec(code, module.__dict__)
24
+ exec(code, module.__dict__) # noqa: S102
25
25
 
26
26
  def get_spec(self):
27
27
  return importlib.machinery.ModuleSpec(self.MOD_NAME, self, origin=self.get_filename(self.MOD_NAME))