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
@@ -50,7 +50,7 @@ class TestExportSourceModel:
50
50
  parser_handler_factory.create() # noise
51
51
 
52
52
  # check with customer queryset and filtering
53
- query_str, query_params = ParserHandler.objects.filter(handler=ph1.handler).query.sql_with_params()
53
+ query_str, query_params = ParserHandler.objects.filter(parser=ph1.parser).query.sql_with_params()
54
54
  export_source = export_source_factory.create(query_str=query_str, query_params=query_params)
55
55
 
56
56
  assert set(export_source.queryset) == {ph1}
@@ -128,7 +128,7 @@ class TestImportSourceModel:
128
128
  assert model.name != comparison_model.name
129
129
 
130
130
  def test_process_wrongly_formatted_import_data(self, handler, import_source):
131
- with pytest.raises(Exception):
131
+ with pytest.raises(KeyError):
132
132
  handler.process(dict(a=1, b="b"))
133
133
 
134
134
  def test_process_basic(self, handler, import_source, parser_handler_factory):
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import logging
2
3
  from io import BytesIO
3
4
  from unittest.mock import patch
4
5
 
@@ -14,7 +15,6 @@ from faker import Faker
14
15
  from pytest_factoryboy import LazyFixture
15
16
 
16
17
  from ..backends.abstract import AbstractDataBackend
17
- from ..exceptions import ImportError
18
18
  from ..models import (
19
19
  ImportCredential,
20
20
  ImportSource,
@@ -25,6 +25,8 @@ from ..models import (
25
25
  )
26
26
  from ..utils import nest_row
27
27
 
28
+ logger = logging.getLogger("io")
29
+
28
30
  fake = Faker()
29
31
 
30
32
 
@@ -33,7 +35,13 @@ def parse(data):
33
35
 
34
36
 
35
37
  def test_nest_row():
36
- a = {"nest1__field1": "val1", "nest2__field2": "val2", "field3": "val3", "nest1__field4": "val4", "field4": "val5"}
38
+ a = {
39
+ "nest1__field1": "val1",
40
+ "nest2__field2": "val2",
41
+ "field3": "val3",
42
+ "nest1__field4": "val4",
43
+ "field4": "val5",
44
+ }
37
45
  expected_res = {
38
46
  "nest1": {
39
47
  "field1": "val1",
@@ -99,7 +107,7 @@ class TestSourceModel:
99
107
  assert source.crontab_repr == ""
100
108
 
101
109
  @pytest.mark.parametrize("source__crontab", [None, LazyFixture("crontab_schedule")])
102
- @pytest.mark.parametrize("_datetime", [(fake.date_time())])
110
+ @pytest.mark.parametrize("_datetime", [fake.date_time()])
103
111
  def test_is_valid_date(self, source, _datetime):
104
112
  if not source.crontab:
105
113
  assert not source.is_valid_date(_datetime)
@@ -232,7 +240,8 @@ class TestImportSourceModel:
232
240
  @patch.object(ParserHandler, "parse")
233
241
  @patch.object(ParserHandler, "handle")
234
242
  @pytest.mark.parametrize(
235
- "import_source__file, import_source__save_data", [(get_test_file(), True), (get_test_file(), False)]
243
+ "import_source__file, import_source__save_data",
244
+ [(get_test_file(), True), (get_test_file(), False)],
236
245
  )
237
246
  def test_import_data_success(self, mock_handle, mock_parse, import_source):
238
247
  file_data = json.loads(import_source.file.read().decode("utf-8"))
@@ -248,16 +257,12 @@ class TestImportSourceModel:
248
257
  else:
249
258
  assert import_source.data == {}
250
259
 
251
- def test_import_data_error(self, import_source):
260
+ def test_import_data_error(self, caplog, import_source):
252
261
  assert import_source.status == ImportSource.Status.PENDING
253
- with pytest.raises(ImportError):
262
+ with caplog.at_level(logging.ERROR):
254
263
  import_source.import_data()
255
264
  assert import_source.status == ImportSource.Status.ERROR
256
265
 
257
- def test_not_silent_raise_error(self, import_source):
258
- with pytest.raises(Exception):
259
- import_source.import_data(silent=False)
260
-
261
266
 
262
267
  @pytest.mark.django_db
263
268
  class TestImportCredentialModel:
@@ -274,8 +279,22 @@ class TestImportCredentialModel:
274
279
  @pytest.mark.parametrize(
275
280
  "type, password, username, authentication_token, certificate_pem, certificate_key",
276
281
  [
277
- (ImportCredential.Type.CREDENTIAL, None, "username", None, "certificate_pem", "certificate_key"),
278
- (ImportCredential.Type.CREDENTIAL, "password", None, None, "certificate_pem", "certificate_key"),
282
+ (
283
+ ImportCredential.Type.CREDENTIAL,
284
+ None,
285
+ "username",
286
+ None,
287
+ "certificate_pem",
288
+ "certificate_key",
289
+ ),
290
+ (
291
+ ImportCredential.Type.CREDENTIAL,
292
+ "password",
293
+ None,
294
+ None,
295
+ "certificate_pem",
296
+ "certificate_key",
297
+ ),
279
298
  (
280
299
  ImportCredential.Type.AUTHENTICATION_TOKEN,
281
300
  "password",
@@ -284,7 +303,14 @@ class TestImportCredentialModel:
284
303
  "certificate_pem",
285
304
  "certificate_key",
286
305
  ),
287
- (ImportCredential.Type.CERTIFICATE, "password", "username", "token", None, "certificate_key"),
306
+ (
307
+ ImportCredential.Type.CERTIFICATE,
308
+ "password",
309
+ "username",
310
+ "token",
311
+ None,
312
+ "certificate_key",
313
+ ),
288
314
  ],
289
315
  )
290
316
  def test_unvalid_credential(
@@ -310,7 +336,14 @@ class TestImportCredentialModel:
310
336
  @pytest.mark.parametrize(
311
337
  "type, username, password, authentication_token, certificate_pem, certificate_key",
312
338
  [
313
- (ImportCredential.Type.CREDENTIAL, None, None, fake.random_letters(16), None, None),
339
+ (
340
+ ImportCredential.Type.CREDENTIAL,
341
+ None,
342
+ None,
343
+ fake.random_letters(16),
344
+ None,
345
+ None,
346
+ ),
314
347
  (
315
348
  ImportCredential.Type.AUTHENTICATION_TOKEN,
316
349
  fake.random_letters(8),
@@ -0,0 +1,271 @@
1
+ import pytest
2
+ from pytest import FixtureRequest
3
+ from rest_framework import status
4
+ from rest_framework.test import APIRequestFactory
5
+
6
+ from wbcore.contrib.authentication.factories import SuperUserFactory
7
+ from wbcore.contrib.io.factories import (
8
+ DataBackendFactory,
9
+ ImportSourceFactory,
10
+ ParserHandlerFactory,
11
+ SourceFactory,
12
+ )
13
+ from wbcore.contrib.io.models import DataBackend, ImportSource, ParserHandler
14
+ from wbcore.contrib.io.serializers import (
15
+ ImportSourceModelSerializer,
16
+ ParserHandlerModelSerializer,
17
+ )
18
+ from wbcore.contrib.io.viewsets import (
19
+ DataBackendRepresentationViewSet,
20
+ ImportSourceModelViewSet,
21
+ ImportSourceRepresentationViewSet,
22
+ ParserHandlerModelViewSet,
23
+ ParserHandlerRepresentationViewSet,
24
+ SourceModelViewSet,
25
+ SourceRepresentationViewSet,
26
+ )
27
+
28
+ BATCH_SIZE = 3
29
+
30
+
31
+ @pytest.mark.django_db
32
+ @pytest.mark.with_db
33
+ class TestViewsets:
34
+ @pytest.fixture
35
+ def request_factory(self):
36
+ return APIRequestFactory()
37
+
38
+ @pytest.fixture
39
+ def super_user(self):
40
+ return SuperUserFactory()
41
+
42
+ @pytest.fixture
43
+ def import_sources(self):
44
+ return ImportSourceFactory.create_batch(BATCH_SIZE)
45
+
46
+ @pytest.fixture
47
+ def import_source(self, import_sources):
48
+ return import_sources[0]
49
+
50
+ @pytest.fixture
51
+ def import_source_build(self):
52
+ parser_handler = ParserHandlerFactory()
53
+ source = SourceFactory()
54
+
55
+ instance = ImportSourceFactory.build(parser_handler=None, source=None)
56
+
57
+ serialized_data = ImportSourceModelSerializer(instance).data
58
+
59
+ serialized_data["parser_handler"] = parser_handler.id
60
+ serialized_data["source"] = source.id
61
+
62
+ return serialized_data
63
+
64
+ @pytest.fixture
65
+ def sources(self):
66
+ return SourceFactory.create_batch(BATCH_SIZE)
67
+
68
+ @pytest.fixture
69
+ def source(self, sources):
70
+ return sources[0]
71
+
72
+ @pytest.fixture
73
+ def parser_handlers(self):
74
+ return ParserHandlerFactory.create_batch(BATCH_SIZE)
75
+
76
+ @pytest.fixture
77
+ def parser_handler(self, parser_handlers):
78
+ return parser_handlers[0]
79
+
80
+ @pytest.fixture
81
+ def parser_handler_build(self):
82
+ instance = ParserHandlerFactory.build()
83
+ return ParserHandlerModelSerializer(instance).data
84
+
85
+ @pytest.fixture
86
+ def data_backends(self):
87
+ DataBackend.objects.all().delete()
88
+ return DataBackendFactory.create_batch(BATCH_SIZE)
89
+
90
+ @pytest.fixture
91
+ def data_backend(self, data_backends):
92
+ return data_backends[0]
93
+
94
+ @pytest.mark.parametrize(
95
+ "vs, batch",
96
+ [
97
+ (ImportSourceRepresentationViewSet, "import_sources"),
98
+ (ImportSourceModelViewSet, "import_sources"),
99
+ (SourceRepresentationViewSet, "sources"),
100
+ (SourceModelViewSet, "sources"),
101
+ (ParserHandlerRepresentationViewSet, "parser_handlers"),
102
+ (ParserHandlerModelViewSet, "parser_handlers"),
103
+ (DataBackendRepresentationViewSet, "data_backends"),
104
+ ],
105
+ )
106
+ def test_get(self, request_factory, super_user, vs, batch, request: FixtureRequest):
107
+ # Arrange
108
+ request.getfixturevalue(batch)
109
+ get_request = request_factory.get("")
110
+ get_request.user = super_user
111
+ viewset = vs.as_view({"get": "list"})
112
+ # Act
113
+ response = viewset(get_request)
114
+ # Assert
115
+ assert len(response.data["results"]) == BATCH_SIZE
116
+ assert response.status_code == status.HTTP_200_OK
117
+
118
+ @pytest.mark.parametrize(
119
+ "vs, instance",
120
+ [
121
+ (ImportSourceRepresentationViewSet, "import_source"),
122
+ (ImportSourceModelViewSet, "import_source"),
123
+ (SourceRepresentationViewSet, "source"),
124
+ (SourceModelViewSet, "source"),
125
+ (ParserHandlerRepresentationViewSet, "parser_handler"),
126
+ (ParserHandlerModelViewSet, "parser_handler"),
127
+ (DataBackendRepresentationViewSet, "data_backend"),
128
+ ],
129
+ )
130
+ def test_retrieve(self, request_factory, super_user, vs, instance, request: FixtureRequest):
131
+ # Arrange
132
+ entry = request.getfixturevalue(instance)
133
+ get_request = request_factory.get("")
134
+ get_request.user = super_user
135
+ viewset = vs.as_view({"get": "retrieve"})
136
+ # We need to work with lookup_field since SourceModelViewSet uses uuid as lookup field
137
+ lookup_field = getattr(vs, "lookup_field", "pk")
138
+ lookup_value = getattr(entry, lookup_field)
139
+ # Act
140
+ response = viewset(get_request, **{lookup_field: lookup_value})
141
+ lookup_field = "id" if lookup_field != "uuid" else "uuid"
142
+ instance = response.data.get("instance")
143
+ # Assert
144
+ assert instance is not None
145
+ assert str(instance[lookup_field]) == str(lookup_value)
146
+ assert not response.data.get("results")
147
+ assert response.status_code == status.HTTP_200_OK
148
+
149
+ @pytest.mark.parametrize(
150
+ "vs, data, creatable",
151
+ [
152
+ (ImportSourceModelViewSet, "import_source_build", True),
153
+ (ParserHandlerModelViewSet, "parser_handler_build", False),
154
+ ],
155
+ )
156
+ def test_create(self, request_factory, super_user, vs, data, creatable, request: FixtureRequest):
157
+ # Arrange
158
+ build_data = request.getfixturevalue(data)
159
+ post_request = request_factory.post("", data=build_data, format="json")
160
+ post_request.user = super_user
161
+ viewset = vs.as_view({"post": "create"})
162
+ # Act
163
+ response = viewset(post_request)
164
+ expected_status_code = status.HTTP_201_CREATED if creatable else status.HTTP_405_METHOD_NOT_ALLOWED
165
+ # Assert
166
+ assert response.status_code == expected_status_code
167
+
168
+ @pytest.mark.parametrize(
169
+ "vs, batch, model, deletable",
170
+ [
171
+ (ImportSourceModelViewSet, "import_sources", ImportSource, True),
172
+ (ParserHandlerModelViewSet, "parser_handlers", ParserHandler, False),
173
+ ],
174
+ )
175
+ def test_delete(
176
+ self,
177
+ request_factory,
178
+ super_user,
179
+ vs,
180
+ batch,
181
+ model,
182
+ deletable,
183
+ request: FixtureRequest,
184
+ ):
185
+ # Arrange
186
+ entries = request.getfixturevalue(batch)
187
+ entry_id = entries[0].id
188
+ delete_request = request_factory.delete("", args=entry_id)
189
+ delete_request.user = super_user
190
+ viewset = vs.as_view({"delete": "destroy"})
191
+ # Act
192
+ response = viewset(delete_request, pk=entry_id)
193
+ # Assert
194
+ if deletable:
195
+ assert response.status_code == status.HTTP_204_NO_CONTENT
196
+ assert model.objects.count() == BATCH_SIZE - 1
197
+ assert not model.objects.filter(id=entry_id).exists()
198
+ else:
199
+ assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
200
+ assert model.objects.count() == BATCH_SIZE
201
+ assert model.objects.filter(id=entry_id).exists()
202
+
203
+ @pytest.mark.parametrize(
204
+ "vs, entry, new_data, updatable",
205
+ [
206
+ (ImportSourceModelViewSet, "import_source", "import_source_build", True),
207
+ (
208
+ ParserHandlerModelViewSet,
209
+ "parser_handler",
210
+ "parser_handler_build",
211
+ False,
212
+ ),
213
+ ],
214
+ )
215
+ def test_put(
216
+ self,
217
+ request_factory,
218
+ super_user,
219
+ vs,
220
+ entry,
221
+ new_data,
222
+ updatable,
223
+ request: FixtureRequest,
224
+ ):
225
+ # Arrange
226
+ entry = request.getfixturevalue(entry)
227
+ new_data = request.getfixturevalue(new_data)
228
+ new_data["id"] = entry.id
229
+ put_request = request_factory.put("", data=new_data, format="json")
230
+ put_request.user = super_user
231
+ viewset = vs.as_view({"put": "update"})
232
+ # Act
233
+ response = viewset(put_request, pk=entry.id)
234
+ expected_status_code = status.HTTP_200_OK if updatable else status.HTTP_405_METHOD_NOT_ALLOWED
235
+ # Assert
236
+ assert response.status_code == expected_status_code
237
+
238
+ @pytest.mark.parametrize(
239
+ "vs, entry, field, updatable",
240
+ [
241
+ (ImportSourceModelViewSet, "import_source", "origin", True),
242
+ (ParserHandlerModelViewSet, "parser_handler", "parser", False),
243
+ ],
244
+ )
245
+ def test_patch(
246
+ self,
247
+ request_factory,
248
+ super_user,
249
+ vs,
250
+ entry,
251
+ field,
252
+ updatable,
253
+ request: FixtureRequest,
254
+ ):
255
+ # Arrange
256
+ instance = request.getfixturevalue(entry)
257
+ old_field_data = getattr(instance, field)
258
+ new_field_data = "Foo Bar"
259
+ patch_request = request_factory.patch("", data={field: new_field_data})
260
+ patch_request.user = super_user
261
+ viewset = vs.as_view({"patch": "partial_update"})
262
+ # Act
263
+ response = viewset(patch_request, pk=instance.id)
264
+ instance.refresh_from_db()
265
+ # Assert
266
+ if updatable:
267
+ assert response.status_code == status.HTTP_200_OK
268
+ assert getattr(instance, field) == new_field_data
269
+ else:
270
+ assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
271
+ assert getattr(instance, field) == old_field_data
@@ -8,7 +8,6 @@ from django.http import HttpResponse
8
8
  from django.utils.functional import cached_property
9
9
  from django.utils.translation import gettext as _
10
10
  from import_export.admin import ImportExportMixin
11
- from import_export.signals import post_export
12
11
  from rest_framework import status
13
12
  from rest_framework.authentication import SessionAuthentication
14
13
  from rest_framework.decorators import action, parser_classes
@@ -20,6 +19,7 @@ from rest_framework.response import Response
20
19
  from wbcore import serializers
21
20
  from wbcore.contrib.authentication.authentication import JWTCookieAuthentication
22
21
  from wbcore.contrib.icons import WBIcon
22
+ from wbcore.contrib.io.import_export.parsers.base_csv import parse
23
23
  from wbcore.enums import RequestType
24
24
  from wbcore.metadata.configs import buttons as bt
25
25
  from wbcore.metadata.configs.display.instance_display import create_simple_display
@@ -51,8 +51,8 @@ class ImportExportDRFMixin(ImportExportMixin):
51
51
  def model(self):
52
52
  try:
53
53
  return getattr(self.queryset, "model", None)
54
- except AttributeError:
55
- raise ParseError("Malformed Queryset")
54
+ except AttributeError as e:
55
+ raise ParseError("Malformed Queryset") from e
56
56
 
57
57
  @cached_property
58
58
  def opts(self):
@@ -70,14 +70,26 @@ class ImportExportDRFMixin(ImportExportMixin):
70
70
  """
71
71
  return ViewResource
72
72
 
73
- def get_import_filter_kwargs(self, request):
74
- return dict()
73
+ def get_import_resource_kwargs(self):
74
+ return {
75
+ "extra_columns_values": self.kwargs, # contains the extra column to inject into the dataset during parsing
76
+ "columns_mapping": {}, # Store the column mapping to rename dataset column during parsing
77
+ }
78
+
79
+ def get_import_parser_path(self) -> str:
80
+ return parse.__module__
75
81
 
76
82
  def get_resource_serializer_class(self):
77
83
  resource_kwargs = {}
78
84
  with suppress(AttributeError, AssertionError):
79
- serializer_class = self.get_serializer_class()
85
+ # we have to mocky patch the action to be "list" because sometime we differentiate the serializer to use
86
+ previous_action = getattr(self, "action", "list")
87
+ self.action = "list"
88
+ serializer_class = getattr(
89
+ self, "serializer_class", self.get_serializer_class()
90
+ ) # we prioritize the default serializer class attribute
80
91
  resource_kwargs["serializer_class_path"] = serializer_class.__module__ + "." + serializer_class.__name__
92
+ self.action = previous_action
81
93
  return resource_kwargs
82
94
 
83
95
  def _get_data_for_export(self, request, queryset, *args, **kwargs) -> tablib.Dataset:
@@ -103,7 +115,7 @@ class ImportExportDRFMixin(ImportExportMixin):
103
115
  detail=False,
104
116
  methods=["PATCH"],
105
117
  permission_classes=[IsAuthenticated],
106
- authentication_classes=[SessionAuthentication, JWTCookieAuthentication],
118
+ # authentication_classes=[SessionAuthentication, JWTCookieAuthentication],
107
119
  url_name="processimport",
108
120
  )
109
121
  @parser_classes([MultiPartParser])
@@ -114,7 +126,7 @@ class ImportExportDRFMixin(ImportExportMixin):
114
126
  if not self.has_import_permission(request):
115
127
  raise PermissionDenied
116
128
  if request.data:
117
- if (file_tmp := request.FILES.get("file")) and (resource_class := self.get_resource_class()):
129
+ if file_tmp := request.FILES.get("file"):
118
130
  from wbcore.contrib.io.models import ( # for circular dependency. Will fix later
119
131
  ImportSource,
120
132
  ParserHandler,
@@ -122,10 +134,14 @@ class ImportExportDRFMixin(ImportExportMixin):
122
134
  )
123
135
 
124
136
  parser_handler, _ = ParserHandler.objects.get_or_create(
125
- handler=f"{self.model._meta.app_label}.{self.model.__name__}", parser=resource_class.__module__
137
+ handler=f"{self.model._meta.app_label}.{self.model.__name__}", parser=self.get_import_parser_path()
126
138
  )
127
139
  import_source = ImportSource.objects.create(
128
- parser_handler=parser_handler, file=file_tmp, origin="Internal Import", creator=request.user
140
+ parser_handler=parser_handler,
141
+ file=file_tmp,
142
+ origin="Internal Import",
143
+ creator=request.user,
144
+ resource_kwargs=self.get_import_resource_kwargs(),
129
145
  )
130
146
  import_data_as_task.delay(import_source.id)
131
147
 
@@ -193,46 +209,24 @@ class ImportExportDRFMixin(ImportExportMixin):
193
209
  """
194
210
  This action export a template into a file
195
211
  """
196
- if (
197
- export_format := int(request.GET.get("export_format", -1))
198
- ) is not None and export_format in ExportFormat.values:
199
- file_format = get_django_import_export_format()[export_format]()
200
- export_data = self.get_export_data(file_format, None, request=request, encoding=self.to_encoding, **kwargs)
201
- content_type = file_format.get_content_type()
202
- response = HttpResponse(export_data, content_type=content_type)
203
- response["Content-Disposition"] = 'attachment; filename="%s"' % (
204
- self.get_export_filename(request, None, file_format),
205
- )
206
- post_export.send(sender=None, model=self.model)
207
- return response
212
+ file_format = get_django_import_export_format(ExportFormat.CSV)()
213
+ export_data = self.get_export_data(file_format, request, None, **kwargs)
214
+ content_type = file_format.get_content_type()
215
+ response = HttpResponse(export_data, content_type=content_type)
216
+ response["Content-Disposition"] = 'attachment; filename="%s"' % (
217
+ self.get_export_filename(request, None, file_format),
218
+ )
219
+ return response
208
220
 
209
221
 
210
222
  @receiver(add_extra_button)
211
223
  def add_template_extra_button(sender, instance, request, view, pk=None, **kwargs):
212
- if (
213
- view
214
- and not instance
215
- and not view.inline
216
- and hasattr(view, "has_import_permission")
217
- and view.has_import_permission(request)
218
- ):
219
- buttons = []
220
- for format in ExportFormat:
221
- buttons.append(
222
- bt.HyperlinkButton(
223
- endpoint=parse_endpoint(request, "export_template", export_format=format.value),
224
- title=format.label,
225
- label=format.label,
226
- )
227
- )
228
- if buttons:
229
- return bt.DropDownButton(
230
- title=_("Templates"),
231
- label=_("Templates"),
232
- icon=WBIcon.UPLOAD.icon,
233
- buttons=tuple(buttons),
234
- weight=2,
235
- )
224
+ if view and not instance and hasattr(view, "has_import_permission") and view.has_import_permission(request):
225
+ return bt.HyperlinkButton(
226
+ endpoint=parse_endpoint(request, "export_template", export_format=ExportFormat.CSV),
227
+ title="Import Template (CSV)",
228
+ label="Import Template (CSV)",
229
+ )
236
230
 
237
231
 
238
232
  @receiver(add_extra_button)
@@ -240,7 +234,6 @@ def add_import_extra_button(sender, instance, request, view, pk=None, **kwargs):
240
234
  if (
241
235
  view
242
236
  and not instance
243
- and not view.inline
244
237
  and hasattr(view, "has_import_permission")
245
238
  and view.has_import_permission(request)
246
239
  and view.get_resource_class()
@@ -262,13 +255,7 @@ def add_import_extra_button(sender, instance, request, view, pk=None, **kwargs):
262
255
 
263
256
  @receiver(add_extra_button)
264
257
  def add_export_extra_button(sender, instance, request, view, pk=None, **kwargs):
265
- if (
266
- view
267
- and not instance
268
- and not view.inline
269
- and hasattr(view, "has_export_permission")
270
- and view.has_export_permission(request)
271
- ):
258
+ if view and not instance and hasattr(view, "has_export_permission") and view.has_export_permission(request):
272
259
  return bt.ActionButton(
273
260
  method=RequestType.PATCH,
274
261
  icon=WBIcon.DOWNLOAD.icon,
@@ -59,4 +59,5 @@ class NotificationTypeModelAdmin(admin.ModelAdmin):
59
59
  "default_enable_web",
60
60
  "default_enable_mobile",
61
61
  "default_enable_email",
62
+ "is_lock",
62
63
  )
@@ -16,7 +16,7 @@ def notification_type_post_migrate(app_config, using=DEFAULT_DB_ALIAS, **kwargs)
16
16
  contenttype = ContentType.objects.db_manager(using).get_for_model(klass, for_concrete_model=False) # type: ignore
17
17
  NotificationType.objects.db_manager(using).filter(contenttype=contenttype).update(stale=True)
18
18
 
19
- for code, title, help_text, web, mobile, email, resource_button_label in getattr(
19
+ for code, title, help_text, web, mobile, email, resource_button_label, is_lock in getattr(
20
20
  klass._meta, "notification_types", []
21
21
  ):
22
22
  NotificationType.objects.using(using).update_or_create(
@@ -30,6 +30,7 @@ def notification_type_post_migrate(app_config, using=DEFAULT_DB_ALIAS, **kwargs)
30
30
  "default_enable_email": email,
31
31
  "resource_button_label": resource_button_label,
32
32
  "stale": False,
33
+ "is_lock": is_lock,
33
34
  },
34
35
  )
35
36
 
@@ -5,9 +5,7 @@ from wbcore.contrib.notifications.models import Notification
5
5
 
6
6
  class AbstractNotificationBackend(ABC):
7
7
  @abstractclassmethod
8
- def send_notification(cls, notification: Notification):
9
- ...
8
+ def send_notification(cls, notification: Notification): ...
10
9
 
11
10
  @abstractclassmethod
12
- def get_configuration(cls) -> dict:
13
- ...
11
+ def get_configuration(cls) -> dict: ...
@@ -5,6 +5,7 @@ import firebase_admin
5
5
  from django.utils.html import strip_tags
6
6
  from firebase_admin import messaging
7
7
  from firebase_admin.credentials import Certificate
8
+ from firebase_admin.exceptions import InvalidArgumentError
8
9
 
9
10
  from wbcore.contrib.notifications.backends.abstract_backend import (
10
11
  AbstractNotificationBackend,
@@ -70,8 +71,10 @@ class NotificationBackend(AbstractNotificationBackend):
70
71
  )
71
72
  try:
72
73
  messaging.send(message, False, app)
73
- except messaging.UnregisteredError:
74
+ except (messaging.UnregisteredError, messaging.QuotaExceededError):
74
75
  expired_tokens.append(token)
76
+ except InvalidArgumentError: # this happens if the body is too big for the mobile push
77
+ pass
75
78
 
76
79
  for token in tokens.filter(device_type=NotificationUserToken.NotificationDeviceType.WEB):
77
80
  data = {
@@ -89,7 +92,7 @@ class NotificationBackend(AbstractNotificationBackend):
89
92
  )
90
93
  try:
91
94
  messaging.send(message, False, app)
92
- except messaging.UnregisteredError:
95
+ except (messaging.UnregisteredError, messaging.QuotaExceededError):
93
96
  expired_tokens.append(token)
94
97
 
95
98
  for expired_token in expired_tokens: