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,6 +1,100 @@
1
1
  import pytest
2
+ from rest_framework import status
3
+
4
+ from wbcore.contrib.geography.factories import ContinentFactory
5
+ from wbcore.contrib.geography.models import Geography
6
+ from wbcore.contrib.geography.serializers import GeographyModelSerializer
7
+ from wbcore.contrib.geography.viewsets import (
8
+ GeographyModelViewSet,
9
+ GeographyRepresentationViewSet,
10
+ )
2
11
 
3
12
 
4
13
  @pytest.mark.django_db
5
- class TestSpecificSViewsets:
6
- pass
14
+ @pytest.mark.with_db
15
+ class TestViewsets:
16
+ @pytest.fixture
17
+ def continents(self):
18
+ return ContinentFactory.create_batch(3)
19
+
20
+ @pytest.fixture
21
+ def continent(self, continents):
22
+ return continents[0]
23
+
24
+ @pytest.fixture
25
+ def continent_build(self):
26
+ instance = ContinentFactory.build()
27
+ return GeographyModelSerializer(instance).data
28
+
29
+ @pytest.mark.parametrize("viewset", [GeographyModelViewSet, GeographyRepresentationViewSet])
30
+ def test_get(self, viewset, request_factory, super_user, continents):
31
+ # Arrange
32
+ request = request_factory.get("")
33
+ request.user = super_user
34
+ viewset = viewset.as_view({"get": "list"})
35
+ # Act
36
+ response = viewset(request)
37
+ # Assert
38
+ assert len(response.data["results"]) == 3
39
+ assert response.status_code == status.HTTP_200_OK
40
+
41
+ @pytest.mark.parametrize("viewset", [GeographyModelViewSet, GeographyRepresentationViewSet])
42
+ def test_retrieve(self, viewset, request_factory, super_user, continent):
43
+ # Arrange
44
+ request = request_factory.get("")
45
+ request.user = super_user
46
+ viewset = viewset.as_view({"get": "retrieve"})
47
+ # Act
48
+ response = viewset(request, pk=continent.id)
49
+ instance = response.data.get("instance")
50
+ # Assert
51
+ assert instance is not None
52
+ assert instance["id"] == continent.id
53
+ assert response.status_code == status.HTTP_200_OK
54
+
55
+ def test_create(self, request_factory, super_user, continent_build):
56
+ # Arrange
57
+ request = request_factory.post("", data=continent_build, format="json")
58
+ request.user = super_user
59
+ viewset = GeographyModelViewSet.as_view({"post": "create"})
60
+ # Act
61
+ response = viewset(request)
62
+ # Assert
63
+ assert response.status_code == status.HTTP_201_CREATED
64
+
65
+ def test_delete(self, request_factory, super_user, continents):
66
+ # Arrange
67
+ entry_id = continents[1].id
68
+ request = request_factory.delete("", args=entry_id)
69
+ request.user = super_user
70
+ viewset = GeographyModelViewSet.as_view({"delete": "destroy"})
71
+ # Act
72
+ response = viewset(request, pk=entry_id)
73
+ # Assert
74
+ assert response.status_code == status.HTTP_204_NO_CONTENT
75
+ assert Geography.objects.count() == 2
76
+ assert not Geography.objects.filter(id=entry_id).exists()
77
+
78
+ def test_put(self, request_factory, super_user, continent, continent_build):
79
+ # Arrange
80
+ continent_build["id"] = continent.id
81
+ request = request_factory.put("", data=continent_build, format="json")
82
+ request.user = super_user
83
+ viewset = GeographyModelViewSet.as_view({"put": "update"})
84
+ # Act
85
+ response = viewset(request, pk=continent.id)
86
+ # Assert
87
+ assert response.status_code == status.HTTP_200_OK
88
+
89
+ def test_patch(self, request_factory, super_user, continent):
90
+ # Arrange
91
+ new_field_data = "Foo Bar"
92
+ request = request_factory.patch("", data={"name": new_field_data})
93
+ request.user = super_user
94
+ viewset = GeographyModelViewSet.as_view({"patch": "partial_update"})
95
+ # Act
96
+ response = viewset(request, pk=continent.id)
97
+ continent.refresh_from_db()
98
+ # Assert
99
+ assert response.status_code == status.HTTP_200_OK
100
+ assert continent.name == new_field_data
@@ -4,8 +4,7 @@ from wbcore.contrib.guardian.models.mixins import PermissionObjectModelMixin
4
4
 
5
5
  @pytest.fixture
6
6
  def mocked_permission_object_model_mixin(mocker):
7
- def __init__(self):
8
- ...
7
+ def __init__(self): ... # noqa
9
8
 
10
9
  PermissionObjectModelMixin.__init__ = __init__
11
10
  PermissionObjectModelMixin.objects = mocker.Mock()
@@ -22,8 +21,8 @@ class TestPermissionObjectModelMixin:
22
21
  def test_save_run_assign_permissions(self, mocker, mocked_permission_object_model_mixin):
23
22
  mocker.patch("django.db.models.Model.save")
24
23
  on_commit = mocker.patch("wbcore.contrib.guardian.models.mixins.transaction.on_commit")
25
- ContentType = mocker.patch("wbcore.contrib.guardian.models.mixins.ContentType")
26
- ContentType.objects.get_for_model.return_value = mocker.Mock()
24
+ content_type_class = mocker.patch("wbcore.contrib.guardian.models.mixins.ContentType")
25
+ content_type_class.objects.get_for_model.return_value = mocker.Mock()
27
26
 
28
27
  mocked_permission_object_model_mixin().save()
29
28
 
@@ -8,39 +8,39 @@ from wbcore.contrib.guardian.models.mixins import (
8
8
 
9
9
  class TestAssignUserPermissionsForObjectAsTask:
10
10
  def test_called(self, mocker):
11
- ContentType = mocker.patch("wbcore.contrib.guardian.models.mixins.ContentType")
11
+ content_type_class = mocker.patch("wbcore.contrib.guardian.models.mixins.ContentType")
12
12
  content_type = mocker.Mock()
13
13
  model_class = mocker.Mock()
14
14
  instance = mocker.Mock()
15
15
  model_class.objects.get.return_value = instance
16
16
  content_type.model_class.return_value = model_class
17
- ContentType.objects.get.return_value = content_type
17
+ content_type_class.objects.get.return_value = content_type
18
18
 
19
19
  assign_user_permissions_for_object_as_task(1, 1)
20
20
 
21
21
  instance.reload_permissions.assert_called_once_with(prune_existing=True)
22
22
 
23
23
  def test_called_with_prune_existing(self, mocker):
24
- ContentType = mocker.patch("wbcore.contrib.guardian.models.mixins.ContentType")
24
+ content_type_class = mocker.patch("wbcore.contrib.guardian.models.mixins.ContentType")
25
25
  content_type = mocker.Mock()
26
26
  model_class = mocker.Mock()
27
27
  instance = mocker.Mock()
28
28
  model_class.objects.get.return_value = instance
29
29
  content_type.model_class.return_value = model_class
30
- ContentType.objects.get.return_value = content_type
30
+ content_type_class.objects.get.return_value = content_type
31
31
 
32
32
  assign_user_permissions_for_object_as_task(1, 1, prune_existing=False)
33
33
 
34
34
  instance.reload_permissions.assert_called_once_with(prune_existing=False)
35
35
 
36
36
  def test_not_called_without_model_class(self, mocker):
37
- ContentType = mocker.patch("wbcore.contrib.guardian.models.mixins.ContentType")
37
+ content_type_class = mocker.patch("wbcore.contrib.guardian.models.mixins.ContentType")
38
38
  content_type = mocker.Mock()
39
39
  model_class = mocker.Mock()
40
40
  instance = mocker.Mock()
41
41
  model_class.objects.get.return_value = instance
42
42
  content_type.model_class.return_value = None
43
- ContentType.objects.get.return_value = content_type
43
+ content_type_class.objects.get.return_value = content_type
44
44
 
45
45
  assign_user_permissions_for_object_as_task(1, 1)
46
46
 
@@ -63,13 +63,13 @@ class TestAssignUserPermissionsForObjectAsTask:
63
63
 
64
64
  class TestAssignObjectPermissionsForUserAsTask:
65
65
  def test_called(self, mocker):
66
- User = mocker.patch("wbcore.contrib.guardian.models.mixins.User")
66
+ user_class = mocker.patch("wbcore.contrib.guardian.models.mixins.User")
67
67
  user = mocker.Mock()
68
68
 
69
- User.objects.get.return_value = user
69
+ user_class.objects.get.return_value = user
70
70
 
71
71
  get_inheriting_subclasses = mocker.patch("wbcore.contrib.guardian.models.mixins.get_inheriting_subclasses")
72
- get_inheriting_subclasses.return_value = [User]
72
+ get_inheriting_subclasses.return_value = [user_class]
73
73
  reload_permissions = mocker.patch("wbcore.contrib.guardian.models.mixins.reload_permissions")
74
74
 
75
75
  assign_object_permissions_for_user_as_task(user_id=1)
@@ -10,7 +10,7 @@ class TestPivotUserObjectPermissionModelViewSet:
10
10
  def test_cached_property_permissions(self, mocker, request):
11
11
  mocked_filter = mocker.patch("wbcore.contrib.guardian.viewsets.viewsets.Permission.objects.filter")
12
12
  view = PivotUserObjectPermissionModelViewSet(request=request, kwargs={"content_type_id": 1})
13
- view.permissions
13
+ assert view.permissions
14
14
 
15
15
  mocked_filter.assert_called_once_with(
16
16
  Q(content_type=1)
@@ -24,7 +24,7 @@ class TestPivotUserObjectPermissionModelViewSet:
24
24
  mocked_get_object_for_this_type = mocker.Mock()
25
25
  mocked_contrib_type.get.return_value = mocked_get_object_for_this_type
26
26
  view = PivotUserObjectPermissionModelViewSet(request=request, kwargs={"content_type_id": 1, "object_pk": 1})
27
- view.linked_object
27
+ assert view.linked_object
28
28
 
29
29
  mocked_get_object_for_this_type.get_object_for_this_type.assert_called_once()
30
30
 
@@ -1,4 +1,4 @@
1
- from .buttons import add_object_permission_button
1
+ from .buttons import add_object_permission_button, PivotUserObjectPermissionButtonViewConfig
2
2
  from .displays import PivotUserObjectPermissionDisplayViewConfig
3
3
  from .endpoints import PivotUserObjectPermissionEndpointViewConfig
4
4
  from .titles import PivotUserObjectPermissionTitleViewConfig
@@ -4,10 +4,19 @@ from django.dispatch import receiver
4
4
  from rest_framework.reverse import reverse
5
5
  from wbcore.contrib.guardian.models.mixins import PermissionObjectModelMixin
6
6
  from wbcore.contrib.icons.icons import WBIcon
7
+ from wbcore.metadata.configs.buttons import ButtonViewConfig
7
8
  from wbcore.metadata.configs.buttons.buttons import WidgetButton
9
+ from wbcore.metadata.configs.buttons.enums import Button
8
10
  from wbcore.signals.instance_buttons import add_extra_button
9
11
 
10
12
 
13
+ class PivotUserObjectPermissionButtonViewConfig(ButtonViewConfig):
14
+ def get_create_buttons(self):
15
+ return {
16
+ Button.SAVE_AND_CLOSE.value,
17
+ }
18
+
19
+
11
20
  @receiver(add_extra_button)
12
21
  def add_object_permission_button(sender, instance, request, view, pk=None, **kwargs):
13
22
  with suppress(AttributeError):
@@ -3,6 +3,13 @@ from wbcore.metadata.configs.endpoints import EndpointViewConfig
3
3
 
4
4
 
5
5
  class PivotUserObjectPermissionEndpointViewConfig(EndpointViewConfig):
6
+ def get_instance_endpoint(self, **kwargs):
7
+ return reverse(
8
+ "wbcore:guardian:pivoteduserobjectpermission-list",
9
+ args=[self.view.kwargs.get("content_type_id", None), self.view.kwargs.get("object_pk", None)],
10
+ request=self.request,
11
+ )
12
+
6
13
  def get_update_endpoint(self, **kwargs):
7
14
  return reverse(
8
15
  "wbcore:guardian:pivoteduserobjectpermission-list",
@@ -8,6 +8,7 @@ from wbcore.contrib.authentication.models.users import Permission, User
8
8
  from wbcore.contrib.authentication.serializers import UserRepresentationSerializer
9
9
  from wbcore.contrib.guardian.models import UserObjectPermission
10
10
  from wbcore.contrib.guardian.viewsets.configs import (
11
+ PivotUserObjectPermissionButtonViewConfig,
11
12
  PivotUserObjectPermissionDisplayViewConfig,
12
13
  PivotUserObjectPermissionEndpointViewConfig,
13
14
  PivotUserObjectPermissionTitleViewConfig,
@@ -17,6 +18,7 @@ from wbcore.contrib.guardian.viewsets.configs import (
17
18
  class PivotUserObjectPermissionModelViewSet(viewsets.ModelViewSet):
18
19
  queryset = UserObjectPermission.objects.all()
19
20
 
21
+ button_config_class = PivotUserObjectPermissionButtonViewConfig
20
22
  display_config_class = PivotUserObjectPermissionDisplayViewConfig
21
23
  endpoint_config_class = PivotUserObjectPermissionEndpointViewConfig
22
24
  title_config_class = PivotUserObjectPermissionTitleViewConfig
@@ -0,0 +1,2 @@
1
+ from .buttons import add_auto_translate_action_button
2
+ from .viewsets import ModelTranslateMixin
@@ -0,0 +1,33 @@
1
+ from django.dispatch import receiver
2
+ from django.urls import resolve
3
+ from rest_framework.request import Request
4
+ from rest_framework.reverse import reverse
5
+
6
+ from wbcore import serializers
7
+ from wbcore.contrib.icons.icons import WBIcon
8
+ from wbcore.enums import RequestType
9
+ from wbcore.metadata.configs.buttons import ActionButton
10
+ from wbcore.metadata.configs.buttons.enums import ButtonDefaultColor
11
+ from wbcore.metadata.configs.display.instance_display.shortcuts import create_simple_display
12
+ from wbcore.signals.instance_buttons import add_extra_button
13
+
14
+
15
+ class AutoTranslateSerializer(serializers.Serializer):
16
+ override_existing_data = serializers.BooleanField(default=False)
17
+
18
+
19
+ @receiver(add_extra_button)
20
+ def add_auto_translate_action_button(sender, instance, request: Request, view, pk=None, **kwargs):
21
+ if instance and pk and view and hasattr(view, "auto_translate"):
22
+ url = reverse(resolve(request.path).view_name.replace("detail", "auto-translate"), args=[pk], request=request)
23
+ return ActionButton(
24
+ method=RequestType.POST,
25
+ icon=WBIcon.DEAL.icon,
26
+ color=ButtonDefaultColor.SUCCESS,
27
+ endpoint=url,
28
+ label="Auto Translate",
29
+ action_label="Auto Translate",
30
+ description_fields="You will automatically translate all fields available for translation.",
31
+ serializer=AutoTranslateSerializer,
32
+ instance_display=create_simple_display([["override_existing_data"]]),
33
+ )
File without changes
@@ -0,0 +1,20 @@
1
+ from django.conf import settings
2
+
3
+ from wbcore.serializers.fields import JSONField
4
+
5
+
6
+ class TranslationJSONField(JSONField):
7
+ field_type = "translation_field"
8
+
9
+ def __init__(self, *args, **kwargs):
10
+ super().__init__(*args, **kwargs)
11
+ self.read_only = False
12
+ self.required = False
13
+ self.allow_null = True
14
+
15
+ def get_representation(self, request, field_name) -> tuple[str, dict]:
16
+ key, representation = super().get_representation(request, field_name)
17
+ representation["default_language"] = settings.LANGUAGE_CODE
18
+ representation["languages"] = settings.MODELTRANS_AVAILABLE_LANGUAGES
19
+ representation["fields"] = self.parent.Meta.model._meta.get_field("i18n").fields
20
+ return key, representation
@@ -0,0 +1,13 @@
1
+ from wbcore import serializers
2
+ from wbcore.contrib.i18n.serializers.fields import TranslationJSONField
3
+
4
+
5
+ class ModelTranslateSerializerMixin(serializers.ModelSerializer):
6
+ _i18n = TranslationJSONField(source="i18n")
7
+
8
+ def update(self, instance, validated_data):
9
+ i18n_data = validated_data.pop("i18n", {}) or {}
10
+ if instance.i18n is None:
11
+ instance.i18n = {}
12
+ instance.i18n.update(i18n_data)
13
+ return super().update(instance, validated_data)
@@ -0,0 +1,11 @@
1
+ import pytest
2
+ from django.apps import apps
3
+ from django.db.backends.base.base import BaseDatabaseWrapper
4
+ from django.db.models.signals import pre_migrate
5
+ from pytest_factoryboy import register
6
+ from wbcore.contrib.authentication.factories import InternalUserFactory, UserFactory
7
+ from wbcore.contrib.geography.tests.signals import app_pre_migration
8
+ from wbcore.tests.conftest import *
9
+
10
+
11
+ pre_migrate.connect(app_pre_migration, sender=apps.get_app_config("geography"))
@@ -0,0 +1,67 @@
1
+ from unittest.mock import MagicMock, patch
2
+
3
+ import pytest
4
+ from wbcore.contrib.i18n.viewsets import ModelTranslateMixin
5
+
6
+
7
+ class TestModel:
8
+ """A simple test model for testing the ModelTranslateMixin."""
9
+
10
+ id = 1
11
+
12
+
13
+ @pytest.fixture
14
+ def viewset():
15
+ """Create a viewset instance for testing."""
16
+
17
+ class TestViewSet(ModelTranslateMixin):
18
+ def get_object(self):
19
+ return TestModel()
20
+
21
+ return TestViewSet()
22
+
23
+
24
+ @patch("wbcore.contrib.i18n.viewsets.translate_model_as_task")
25
+ @patch("wbcore.contrib.i18n.viewsets.ContentType.objects.get_for_model")
26
+ def test_auto_translate_success(mock_get_for_model, mock_translate_task, viewset, rf):
27
+ """Test that auto_translate initiates a translation task and returns a success response."""
28
+ # Setup
29
+ mock_content_type = MagicMock()
30
+ mock_content_type.id = 123
31
+ mock_get_for_model.return_value = mock_content_type
32
+
33
+ mock_translate_task.delay = MagicMock()
34
+
35
+ request = rf.post("/auto-translate/")
36
+ request.data = {}
37
+
38
+ # Execute
39
+ response = viewset.auto_translate(request)
40
+
41
+ # Assert
42
+ mock_get_for_model.assert_called_once()
43
+ mock_translate_task.delay.assert_called_once_with(123, 1, False)
44
+
45
+ assert response.status_code == 200
46
+ assert response.data == {"__notification": "The translation started in the background."}
47
+
48
+
49
+ @patch("wbcore.contrib.i18n.viewsets.translate_model_as_task")
50
+ def test_auto_translate_no_object(mock_translate_task, viewset, rf):
51
+ """Test that auto_translate returns an error response when no object is found."""
52
+ # Setup
53
+ viewset.get_object = MagicMock(return_value=None)
54
+ mock_translate_task.delay = MagicMock()
55
+
56
+ request = rf.post("/auto-translate/")
57
+ request.data = {}
58
+ # Execute
59
+ response = viewset.auto_translate(request)
60
+
61
+ # Assert
62
+ mock_translate_task.delay.assert_not_called()
63
+
64
+ # The viewset code uses status.HTTP_400_BAD_REQUEST
65
+ assert response.status_code == 400
66
+ assert "non_field_errors" in response.data
67
+ assert response.data["non_field_errors"] == ["The URL was malformatted."]
@@ -0,0 +1,140 @@
1
+ from copy import deepcopy
2
+ from typing import TYPE_CHECKING, Any, Iterator
3
+
4
+ from celery import shared_task
5
+ from django.conf import settings
6
+ from django.contrib.contenttypes.models import ContentType
7
+ from django.db.models import Field
8
+ from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage
9
+ from modeltrans.fields import TranslatedVirtualField
10
+
11
+ from wbcore.contrib.ai.llm.utils import run_llm
12
+
13
+ if TYPE_CHECKING:
14
+ from django.db.models import Model
15
+
16
+
17
+ def get_base_prompt(from_language: str, to_language: str) -> list["BaseMessage"]:
18
+ """
19
+ Creates a base prompt for translation with system instructions.
20
+
21
+ Args:
22
+ from_language: ISO2 code of the source language
23
+ to_language: ISO2 code of the target language
24
+
25
+ Returns:
26
+ list[BaseMessage]: A list containing the system message for translation
27
+ """
28
+ return [
29
+ SystemMessage(f"""
30
+ You are a highly skilled translator with expertise in accurately translating content between languages.
31
+ Your task is to translate the provided content from ISO2={from_language} to ISO2={to_language}.
32
+
33
+ Please adhere to the following guidelines:
34
+ - Maintain the original meaning, tone, and style of the content.
35
+ - Preserve any formatting, including line breaks and special characters.
36
+ - Keep proper nouns unchanged unless they have widely accepted translations.
37
+ - Do not add, remove, or alter any information in the content.
38
+ - Provide only the translated text without any additional explanations or notes.
39
+ """)
40
+ ]
41
+
42
+
43
+ def _is_translated_field(field: Field | TranslatedVirtualField) -> bool:
44
+ """
45
+ Determines if a field is a translated field that needs processing.
46
+
47
+ Args:
48
+ field: The field to check
49
+
50
+ Returns:
51
+ bool: True if the field is a TranslatedVirtualField with a non-default language
52
+ """
53
+ return (
54
+ isinstance(field, TranslatedVirtualField)
55
+ and field.language is not None
56
+ and field.language != settings.LANGUAGE_CODE
57
+ )
58
+
59
+
60
+ def _get_translation_fields(
61
+ fields: list[Field | TranslatedVirtualField | Any],
62
+ ) -> Iterator[TranslatedVirtualField]:
63
+ """
64
+ Filters a list of fields to only include translated fields.
65
+
66
+ Args:
67
+ fields: List of model fields to filter
68
+
69
+ Returns:
70
+ Iterator[TranslatedVirtualField]: Iterator yielding only the translated virtual fields
71
+ """
72
+ yield from filter(_is_translated_field, fields) # type: ignore
73
+
74
+
75
+ def translate_string(content: str, language: str) -> str:
76
+ prompt = get_base_prompt(settings.LANGUAGE_CODE, language)
77
+ prompt.append(HumanMessage(content))
78
+ return str(run_llm(prompt)[0])
79
+
80
+
81
+ def translate_dict(content: dict, language: str) -> dict:
82
+ new_content = deepcopy(content)
83
+
84
+ for key, value in content.items():
85
+ if isinstance(value, dict):
86
+ new_content[key] = translate_dict(value, language)
87
+ elif isinstance(value, str):
88
+ new_content[key] = translate_string(value, language)
89
+
90
+ return new_content
91
+
92
+
93
+ def translate_model(model: "Model", override: bool = False):
94
+ """
95
+ Translates all translatable fields in a model instance.
96
+
97
+ Processes all fields in the model that are marked for translation,
98
+ generates translations using an LLM, and saves the model with
99
+ the translated content.
100
+
101
+ Args:
102
+ model: The Django model instance to translate
103
+ override: If True, do not set the translated value if there is already content
104
+ """
105
+ for field in _get_translation_fields(model._meta.get_fields()):
106
+ original_content = getattr(model, field.original_field.name)
107
+ if field.language is None or (field is not None and not override):
108
+ continue
109
+
110
+ if custom_translation := getattr(model, f"_translate_{field.original_field.name}", None):
111
+ setattr(model, field.name, custom_translation(field.language))
112
+ else:
113
+ match original_content:
114
+ case str():
115
+ setattr(
116
+ model,
117
+ field.name,
118
+ translate_string(original_content, field.language),
119
+ )
120
+ case dict():
121
+ setattr(model, field.name, translate_dict(original_content, field.language))
122
+
123
+ model.save()
124
+
125
+
126
+ @shared_task
127
+ def translate_model_as_task(content_type_id: int, pk: Any, override: bool = False):
128
+ """
129
+ Celery task for asynchronous translation of a model instance.
130
+
131
+ Retrieves the model class from the content type ID, loads the
132
+ specific instance by primary key, and translates its fields.
133
+
134
+ Args:
135
+ content_type_id: ID of the ContentType for the model to translate
136
+ pk: Primary key of the specific model instance to translate
137
+ """
138
+ if mc := ContentType.objects.get(id=content_type_id).model_class():
139
+ model = mc.objects.get(pk=pk)
140
+ translate_model(model, override)
@@ -0,0 +1,36 @@
1
+ from django.contrib.contenttypes.models import ContentType
2
+ from rest_framework import status
3
+ from rest_framework.decorators import action
4
+ from rest_framework.request import Request
5
+ from rest_framework.response import Response
6
+
7
+ from wbcore import viewsets
8
+ from wbcore.contrib.i18n.translation import translate_model_as_task
9
+
10
+
11
+ class ModelTranslateMixin(viewsets.ModelViewSet):
12
+ @action(methods=["POST"], detail=True)
13
+ def auto_translate(self, request: Request, *args, **kwargs):
14
+ """Initiates an asynchronous translation task for the specified model instance.
15
+
16
+ This method retrieves the model instance based on the provided URL parameters,
17
+ starts a background translation task, and returns a notification response.
18
+
19
+ Args:
20
+ request (Request): The HTTP request object containing the data for the translation.
21
+ *args: Additional positional arguments.
22
+ **kwargs: Additional keyword arguments.
23
+ override_existing_data (bool): If True, existing content will not be overridden.
24
+
25
+ Returns:
26
+ Response: A response indicating the status of the translation initiation.
27
+ """
28
+ override_existing_data = request.data.get("override_existing_data", "false") == "true"
29
+
30
+ obj = self.get_object()
31
+ if obj:
32
+ ct = ContentType.objects.get_for_model(obj) # type: ignore
33
+ translate_model_as_task.delay(ct.id, obj.id, override_existing_data)
34
+ return Response({"__notification": "The translation started in the background."})
35
+
36
+ return Response({"non_field_errors": ["The URL was malformatted."]}, status=status.HTTP_400_BAD_REQUEST)
@@ -15,6 +15,7 @@ class IconBackend(AbstractBackend):
15
15
  APPROVE = "wb-icon-thumb-up"
16
16
  BANK = "wb-icon-bank"
17
17
  BIRTHDAY = "wb-icon-cake-1"
18
+ BROADCAST = "wb-icon-send"
18
19
  BOOKMARK = "wb-icon-bookmark"
19
20
  CALENDAR = "wb-icon-calendar-1"
20
21
  CHART_AREA = "wb-icon-chart-area"
@@ -19,6 +19,7 @@ class IconBackend(AbstractBackend):
19
19
  BANK = "account_balance"
20
20
  BIRTHDAY = "cake"
21
21
  BOOKMARK = "bookmark_border"
22
+ BROADCAST = "arrow_split"
22
23
  CALENDAR = "calendar_month"
23
24
  CHART_AREA = "stacked_line_chart"
24
25
  CHART_BARS_HORIZONTAL = "align_horizontal_center"
@@ -15,20 +15,16 @@ FALLBACK_ICON_VALUE = "FALLBACK_ICON"
15
15
  class WBIconMeta(ChoicesMeta):
16
16
  """A metaclass for creating a enum choices."""
17
17
 
18
- def __new__(metacls, classname, bases, classdict, **kwds):
18
+ def __new__(cls, classname, bases, classdict, **kwds):
19
19
  dict.__setitem__(
20
20
  classdict, FALLBACK_ICON_VALUE, (FALLBACK_ICON_VALUE, "Fallback Icon")
21
21
  ) # add a default member that represent the fallback in case it's not yet implement in the imported backend
22
- cls = super().__new__(metacls, classname, bases, classdict, **kwds)
22
+ cls = super().__new__(cls, classname, bases, classdict, **kwds)
23
23
  with suppress(ModuleNotFoundError):
24
- setattr(
25
- cls,
26
- "icon_backend",
27
- import_from_dotted_path(getattr(settings, "WBCORE_ICON_BACKEND", DEFAULT_ICON_BACKEND)),
28
- )
24
+ cls.icon_backend = import_from_dotted_path(getattr(settings, "WBCORE_ICON_BACKEND", DEFAULT_ICON_BACKEND))
29
25
  icon_backend = cls.icon_backend
30
26
  # For each enumeration values, attached an "icon" property to its members
31
- for member, value in zip(cls.__members__.values(), cls.values):
27
+ for member, value in zip(cls.__members__.values(), cls.values, strict=False):
32
28
  member._icon_ = getattr(icon_backend, value, icon_backend.fallback_icon)
33
29
  return enum.unique(cls)
34
30
 
@@ -54,6 +50,7 @@ class WBIcon(TextChoices, metaclass=WBIconMeta):
54
50
  BANK = "BANK", "Bank"
55
51
  BIRTHDAY = "BIRTHDAY", "Birthday"
56
52
  BOOKMARK = "BOOKMARK", "Bookmark"
53
+ BROADCAST = "BROADCAST", "Broadcast"
57
54
  CALENDAR = "CALENDAR", "Calendar"
58
55
  CHART_AREA = "CHART_AREA", "Chart area"
59
56
  CHART_BARS_HORIZONTAL = "CHART_BARS_HORIZONTAL", "Chart bars horizonal"
@@ -133,6 +133,7 @@ class ImportSourceModelAdmin(admin.ModelAdmin):
133
133
  ("parser_handler", "source", "origin"),
134
134
  ("file", "save_data", "progress_index"),
135
135
  ("creator",),
136
+ ("resource_kwargs",),
136
137
  ("data",),
137
138
  ("log",),
138
139
  ("errors_log",),