wbcore 1.54.10__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 (153) hide show
  1. wbcore/cache/decorators.py +3 -3
  2. wbcore/cache/registry.py +3 -2
  3. wbcore/configs/decorators.py +1 -1
  4. wbcore/configurations/configurations/apps.py +2 -2
  5. wbcore/configurations/configurations/authentication.py +1 -1
  6. wbcore/configurations/configurations/base.py +1 -1
  7. wbcore/configurations/configurations/cache.py +1 -1
  8. wbcore/configurations/configurations/maintenance.py +1 -1
  9. wbcore/configurations/configurations/media.py +1 -1
  10. wbcore/configurations/configurations/middleware.py +1 -1
  11. wbcore/configurations/configurations/rest_framework.py +1 -1
  12. wbcore/configurations/configurations/static.py +1 -1
  13. wbcore/configurations/configurations/wbcore.py +1 -1
  14. wbcore/content_type/serializers.py +1 -1
  15. wbcore/content_type/utils.py +3 -3
  16. wbcore/contrib/agenda/viewsets/calendar_items.py +7 -7
  17. wbcore/contrib/ai/llm/config.py +1 -1
  18. wbcore/contrib/authentication/admin.py +2 -2
  19. wbcore/contrib/authentication/filters.py +0 -1
  20. wbcore/contrib/authentication/models/users.py +3 -3
  21. wbcore/contrib/authentication/models/users_activities.py +1 -1
  22. wbcore/contrib/authentication/serializers/users.py +2 -2
  23. wbcore/contrib/authentication/tests/test_tokens.py +3 -3
  24. wbcore/contrib/authentication/tests/test_users.py +0 -1
  25. wbcore/contrib/authentication/viewsets/user_activities.py +2 -1
  26. wbcore/contrib/authentication/viewsets/users.py +6 -4
  27. wbcore/contrib/color/models.py +2 -1
  28. wbcore/contrib/currency/factories.py +1 -1
  29. wbcore/contrib/currency/import_export/backends/fixerio/currency_fx_rates.py +3 -1
  30. wbcore/contrib/currency/models.py +28 -8
  31. wbcore/contrib/currency/serializers.py +5 -1
  32. wbcore/contrib/currency/tests/test_serializers.py +7 -3
  33. wbcore/contrib/currency/tests/test_viewsets.py +1 -1
  34. wbcore/contrib/currency/viewsets/currency.py +2 -2
  35. wbcore/contrib/dataloader/utils.py +2 -2
  36. wbcore/contrib/directory/factories/__init__.py +1 -1
  37. wbcore/contrib/directory/factories/entries.py +1 -1
  38. wbcore/contrib/directory/models/contacts.py +2 -2
  39. wbcore/contrib/directory/models/entries.py +18 -4
  40. wbcore/contrib/directory/models/relationships.py +25 -30
  41. wbcore/contrib/directory/permissions.py +6 -0
  42. wbcore/contrib/directory/serializers/companies.py +15 -8
  43. wbcore/contrib/directory/serializers/contacts.py +8 -8
  44. wbcore/contrib/directory/serializers/entries.py +24 -15
  45. wbcore/contrib/directory/serializers/entry_representations.py +4 -2
  46. wbcore/contrib/directory/serializers/persons.py +8 -9
  47. wbcore/contrib/directory/serializers/relationships.py +2 -2
  48. wbcore/contrib/directory/tests/conftest.py +2 -0
  49. wbcore/contrib/directory/tests/disable_signals.py +11 -1
  50. wbcore/contrib/directory/tests/signals.py +2 -2
  51. wbcore/contrib/directory/tests/test_models.py +88 -66
  52. wbcore/contrib/directory/tests/test_serializers.py +1 -1
  53. wbcore/contrib/directory/tests/test_viewsets.py +8 -8
  54. wbcore/contrib/directory/viewsets/buttons/__init__.py +1 -1
  55. wbcore/contrib/directory/viewsets/buttons/relationships.py +32 -0
  56. wbcore/contrib/directory/viewsets/contacts.py +6 -6
  57. wbcore/contrib/directory/viewsets/display/__init__.py +1 -1
  58. wbcore/contrib/directory/viewsets/display/entries.py +51 -36
  59. wbcore/contrib/directory/viewsets/display/relationships.py +22 -22
  60. wbcore/contrib/directory/viewsets/entries.py +4 -5
  61. wbcore/contrib/directory/viewsets/previews/entries.py +3 -3
  62. wbcore/contrib/directory/viewsets/relationships.py +16 -2
  63. wbcore/contrib/directory/viewsets/titles/relationships.py +2 -3
  64. wbcore/contrib/documents/filters.py +0 -2
  65. wbcore/contrib/example_app/models.py +4 -4
  66. wbcore/contrib/example_app/serializers/person_team.py +4 -4
  67. wbcore/contrib/example_app/tests/e2e/test_teams.py +1 -1
  68. wbcore/contrib/geography/tests/test_viewsets.py +1 -1
  69. wbcore/contrib/guardian/tests/test_model_mixins.py +3 -3
  70. wbcore/contrib/guardian/tests/test_tasks.py +9 -9
  71. wbcore/contrib/guardian/tests/test_viewsets.py +2 -2
  72. wbcore/contrib/icons/backends/default.py +1 -0
  73. wbcore/contrib/icons/backends/material.py +1 -0
  74. wbcore/contrib/icons/icons.py +5 -8
  75. wbcore/contrib/io/exceptions.py +8 -0
  76. wbcore/contrib/io/import_export/backends/stream.py +2 -2
  77. wbcore/contrib/io/imports.py +10 -5
  78. wbcore/contrib/io/models.py +17 -14
  79. wbcore/contrib/io/serializers.py +2 -2
  80. wbcore/contrib/io/tests/test_backends.py +1 -1
  81. wbcore/contrib/io/tests/test_imports.py +1 -1
  82. wbcore/contrib/io/viewset_mixins.py +4 -4
  83. wbcore/contrib/notifications/dispatch.py +18 -7
  84. wbcore/contrib/pandas/filterset.py +8 -7
  85. wbcore/contrib/pandas/views.py +7 -5
  86. wbcore/contrib/tags/models/tags.py +4 -1
  87. wbcore/contrib/workflow/factories/display.py +2 -2
  88. wbcore/contrib/workflow/models/data.py +7 -4
  89. wbcore/contrib/workflow/models/process.py +2 -2
  90. wbcore/contrib/workflow/serializers/data.py +8 -8
  91. wbcore/contrib/workflow/tests/test_models/test_condition.py +1 -1
  92. wbcore/contrib/workflow/workflows/assignees.py +4 -4
  93. wbcore/dynamic_preferences_registry.py +23 -9
  94. wbcore/enums.py +2 -1
  95. wbcore/filters/fields/content_type.py +5 -4
  96. wbcore/filters/fields/datetime.py +34 -9
  97. wbcore/filters/fields/models.py +2 -2
  98. wbcore/filters/filterset.py +22 -6
  99. wbcore/filters/mixins.py +6 -2
  100. wbcore/forms.py +6 -6
  101. wbcore/fsm/markdown_extensions.py +1 -1
  102. wbcore/fsm/mixins.py +7 -4
  103. wbcore/markdown/models.py +8 -5
  104. wbcore/metadata/configs/buttons/bases.py +6 -6
  105. wbcore/metadata/configs/buttons/buttons.py +2 -1
  106. wbcore/metadata/configs/buttons/view_config.py +5 -3
  107. wbcore/metadata/configs/display/display.py +2 -2
  108. wbcore/metadata/configs/display/formatting.py +6 -7
  109. wbcore/metadata/configs/display/list_display.py +6 -7
  110. wbcore/metadata/configs/display/models.py +6 -0
  111. wbcore/metadata/configs/fields.py +6 -1
  112. wbcore/metadata/configs/filter_fields.py +12 -11
  113. wbcore/models/fields.py +2 -2
  114. wbcore/permissions/permissions.py +2 -2
  115. wbcore/permissions/utils.py +2 -2
  116. wbcore/reversion/viewsets/titles.py +4 -3
  117. wbcore/serializers/__init__.py +1 -0
  118. wbcore/serializers/fields/__init__.py +1 -0
  119. wbcore/serializers/fields/datetime.py +35 -6
  120. wbcore/serializers/fields/fields.py +1 -1
  121. wbcore/serializers/fields/fsm.py +1 -1
  122. wbcore/serializers/fields/list.py +1 -1
  123. wbcore/serializers/fields/mixins.py +13 -5
  124. wbcore/serializers/fields/related.py +4 -6
  125. wbcore/serializers/fields/text.py +1 -1
  126. wbcore/serializers/fields/types.py +1 -0
  127. wbcore/serializers/serializers.py +6 -2
  128. wbcore/tasks.py +2 -2
  129. wbcore/templates/wbcore/email_base_template.html +3 -3
  130. wbcore/test/e2e_helpers_methods/e2e_checks.py +10 -4
  131. wbcore/test/e2e_helpers_methods/e2e_helper_methods.py +4 -2
  132. wbcore/test/mixins.py +1 -1
  133. wbcore/test/tests.py +6 -9
  134. wbcore/test/utils.py +3 -4
  135. wbcore/tests/e2e/test_e2e.py +2 -2
  136. wbcore/tests/test_cache/test_decorators.py +3 -3
  137. wbcore/tests/test_configs.py +1 -1
  138. wbcore/tests/test_fields/test_number_fields.py +1 -1
  139. wbcore/tests/test_filters/test_mixins.py +3 -3
  140. wbcore/tests/test_models/test_mixins.py +1 -1
  141. wbcore/tests/test_utils/test_date.py +1 -1
  142. wbcore/tests/test_utils/test_date_builder.py +25 -1
  143. wbcore/utils/date.py +18 -2
  144. wbcore/utils/figures.py +2 -2
  145. wbcore/utils/models.py +3 -2
  146. wbcore/utils/reportlab.py +7 -0
  147. wbcore/utils/rrules.py +1 -1
  148. wbcore/utils/string_loader.py +1 -1
  149. wbcore/utils/strings.py +2 -2
  150. wbcore/viewsets/mixins.py +6 -4
  151. {wbcore-1.54.10.dist-info → wbcore-1.58.2.dist-info}/METADATA +2 -1
  152. {wbcore-1.54.10.dist-info → wbcore-1.58.2.dist-info}/RECORD +153 -151
  153. {wbcore-1.54.10.dist-info → wbcore-1.58.2.dist-info}/WHEEL +0 -0
@@ -4,7 +4,7 @@ from typing import Optional
4
4
  from django.utils.translation import gettext as _
5
5
 
6
6
  from wbcore.contrib.color.enums import WBColor
7
- from wbcore.contrib.directory.models import ClientManagerRelationship as CMR
7
+ from wbcore.contrib.directory.models import ClientManagerRelationship
8
8
  from wbcore.contrib.icons import WBIcon
9
9
  from wbcore.metadata.configs import display as dp
10
10
  from wbcore.metadata.configs.display.instance_display.shortcuts import (
@@ -15,7 +15,7 @@ from wbcore.metadata.configs.display.instance_display.utils import repeat_field
15
15
  from wbcore.metadata.configs.display.view_config import DisplayViewConfig
16
16
 
17
17
 
18
- class CMR_Color(Enum):
18
+ class ClientManagerRelationshipColor(Enum):
19
19
  DRAFT = WBColor.RED_LIGHT.value
20
20
  PENDING = WBColor.YELLOW_LIGHT.value
21
21
  APPROVED = WBColor.GREEN_LIGHT.value
@@ -203,29 +203,29 @@ class ClientManagerModelDisplay(DisplayViewConfig):
203
203
  key="status",
204
204
  items=[
205
205
  dp.LegendItem(
206
- icon=CMR_Color.DRAFT.value,
207
- label=CMR.Status.DRAFT.label,
208
- value=CMR.Status.DRAFT.name,
206
+ icon=ClientManagerRelationshipColor.DRAFT.value,
207
+ label=ClientManagerRelationship.Status.DRAFT.label,
208
+ value=ClientManagerRelationship.Status.DRAFT.name,
209
209
  ),
210
210
  dp.LegendItem(
211
- icon=CMR_Color.PENDING.value,
212
- label=CMR.Status.PENDINGADD.label,
213
- value=CMR.Status.PENDINGADD.name,
211
+ icon=ClientManagerRelationshipColor.PENDING.value,
212
+ label=ClientManagerRelationship.Status.PENDINGADD.label,
213
+ value=ClientManagerRelationship.Status.PENDINGADD.name,
214
214
  ),
215
215
  dp.LegendItem(
216
- icon=CMR_Color.APPROVED.value,
217
- label=CMR.Status.APPROVED.label,
218
- value=CMR.Status.APPROVED.name,
216
+ icon=ClientManagerRelationshipColor.APPROVED.value,
217
+ label=ClientManagerRelationship.Status.APPROVED.label,
218
+ value=ClientManagerRelationship.Status.APPROVED.name,
219
219
  ),
220
220
  dp.LegendItem(
221
- icon=CMR_Color.PENDING_REMOVE.value,
222
- label=CMR.Status.PENDINGREMOVE.label,
223
- value=CMR.Status.PENDINGREMOVE.name,
221
+ icon=ClientManagerRelationshipColor.PENDING_REMOVE.value,
222
+ label=ClientManagerRelationship.Status.PENDINGREMOVE.label,
223
+ value=ClientManagerRelationship.Status.PENDINGREMOVE.name,
224
224
  ),
225
225
  dp.LegendItem(
226
- icon=CMR_Color.REMOVED.value,
227
- label=CMR.Status.REMOVED.label,
228
- value=CMR.Status.REMOVED.name,
226
+ icon=ClientManagerRelationshipColor.REMOVED.value,
227
+ label=ClientManagerRelationship.Status.REMOVED.label,
228
+ value=ClientManagerRelationship.Status.REMOVED.name,
229
229
  ),
230
230
  ],
231
231
  ),
@@ -242,23 +242,23 @@ class ClientManagerModelDisplay(DisplayViewConfig):
242
242
  formatting_rules=[
243
243
  dp.FormattingRule(
244
244
  style={"backgroundColor": WBColor.YELLOW_LIGHT.value},
245
- condition=("==", CMR.Status.PENDINGADD.name),
245
+ condition=("==", ClientManagerRelationship.Status.PENDINGADD.name),
246
246
  ),
247
247
  dp.FormattingRule(
248
248
  style={"backgroundColor": WBColor.YELLOW_DARK.value},
249
- condition=("==", CMR.Status.PENDINGREMOVE.name),
249
+ condition=("==", ClientManagerRelationship.Status.PENDINGREMOVE.name),
250
250
  ),
251
251
  dp.FormattingRule(
252
252
  style={"backgroundColor": WBColor.GREEN_LIGHT.value},
253
- condition=("==", CMR.Status.APPROVED.name),
253
+ condition=("==", ClientManagerRelationship.Status.APPROVED.name),
254
254
  ),
255
255
  dp.FormattingRule(
256
256
  style={"backgroundColor": WBColor.RED_LIGHT.value},
257
- condition=("==", CMR.Status.DRAFT.name),
257
+ condition=("==", ClientManagerRelationship.Status.DRAFT.name),
258
258
  ),
259
259
  dp.FormattingRule(
260
260
  style={"backgroundColor": WBColor.GREY.value},
261
- condition=("==", CMR.Status.REMOVED.name),
261
+ condition=("==", ClientManagerRelationship.Status.REMOVED.name),
262
262
  ),
263
263
  ],
264
264
  )
@@ -150,11 +150,10 @@ class PersonModelViewSet(ModelTranslateMixin, EntryModelViewSet):
150
150
  return super().get_serializer_class()
151
151
 
152
152
  def get_queryset(self):
153
- return (
154
- super()
155
- .get_queryset()
156
- .annotate(last_connection=Coalesce(UserActivity.get_latest_login_datetime_subquery(), None))
157
- )
153
+ qs = super().get_queryset()
154
+ if "pk" in self.kwargs:
155
+ qs = qs.annotate(last_connection=Coalesce(UserActivity.get_latest_login_datetime_subquery(), None))
156
+ return qs
158
157
 
159
158
 
160
159
  class CompanyModelViewSet(EntryModelViewSet):
@@ -1,3 +1,5 @@
1
+ from contextlib import suppress
2
+
1
3
  from django.utils.translation import gettext as _
2
4
 
3
5
  from wbcore.contrib.icons import WBIcon
@@ -17,12 +19,10 @@ class EntryPreviewConfig(PreviewViewConfig):
17
19
  ["primary_telephone"],
18
20
  ["primary_manager_name"],
19
21
  ]
20
- try:
22
+ with suppress(Exception):
21
23
  entry = self.view.get_object()
22
24
  if entry.profile_image:
23
25
  fields.insert(0, "profile_image")
24
- except Exception:
25
- pass
26
26
 
27
27
  return create_simple_display(fields)
28
28
 
@@ -2,8 +2,10 @@ from django.db.models import CharField, F, OuterRef, Q, Subquery, Value
2
2
  from django.db.models.functions import Concat
3
3
  from django.utils.functional import cached_property
4
4
  from dynamic_preferences.registries import global_preferences_registry
5
- from rest_framework import filters
5
+ from rest_framework import filters, status
6
+ from rest_framework.decorators import action
6
7
  from rest_framework.permissions import IsAuthenticated
8
+ from rest_framework.response import Response
7
9
  from rest_fuzzysearch.search import RankedFuzzySearchFilter
8
10
  from reversion.views import RevisionMixin
9
11
 
@@ -19,6 +21,7 @@ from wbcore.filters import DjangoFilterBackend
19
21
  from wbcore.viewsets import ModelViewSet, ReadOnlyModelViewSet, RepresentationViewSet
20
22
 
21
23
  from ..filters import ClientManagerFilter, RelationshipEntryFilter, RelationshipFilter
24
+ from ..permissions import IsClientManagerRelationshipAdmin
22
25
  from ..serializers import (
23
26
  ClientManagerModelSerializer,
24
27
  ClientManagerRelationshipRepresentationSerializer,
@@ -29,7 +32,7 @@ from ..serializers import (
29
32
  RelationshipTypeRepresentationSerializer,
30
33
  UserIsClientModelSerializer,
31
34
  )
32
- from .buttons import EmployerEmployeeRelationshipButtonConfig
35
+ from .buttons import ClientManagerRelationshipButtonConfig, EmployerEmployeeRelationshipButtonConfig
33
36
  from .display import (
34
37
  ClientManagerModelDisplay,
35
38
  EmployeeEmployerDisplayConfig,
@@ -179,6 +182,7 @@ class ClientManagerViewSet(RevisionMixin, ModelViewSet):
179
182
  queryset = ClientManagerRelationship.objects.none()
180
183
  serializer_class = ClientManagerModelSerializer
181
184
  display_config_class = ClientManagerModelDisplay
185
+ button_config_class = ClientManagerRelationshipButtonConfig
182
186
  endpoint_config_class = ClientManagerEndpoint
183
187
  filterset_class = ClientManagerFilter
184
188
  title_config_class = ClientManagerTitleConfig
@@ -197,6 +201,16 @@ class ClientManagerViewSet(RevisionMixin, ModelViewSet):
197
201
  return ClientManagerRelationship.objects.select_related("client", "relationship_manager")
198
202
  return ClientManagerRelationship.objects.none()
199
203
 
204
+ @action(detail=False, methods=["PATCH"], permission_classes=[IsClientManagerRelationshipAdmin])
205
+ def approveallpendingrequests(self, *args, **kwargs):
206
+ for request in ClientManagerRelationship.objects.filter(status=ClientManagerRelationship.Status.PENDINGADD):
207
+ request.approve(by=self.request.user)
208
+ request.save()
209
+ for request in ClientManagerRelationship.objects.filter(status=ClientManagerRelationship.Status.PENDINGREMOVE):
210
+ request.approveremoval(by=self.request.user)
211
+ request.save()
212
+ return Response({}, status=status.HTTP_200_OK)
213
+
200
214
 
201
215
  class UserIsClientViewSet(ReadOnlyModelViewSet):
202
216
  queryset = ClientManagerRelationship.objects.all()
@@ -1,7 +1,6 @@
1
1
  from django.utils.translation import gettext as _
2
2
 
3
- from wbcore.contrib.directory.models import ClientManagerRelationship as CMR
4
- from wbcore.contrib.directory.models import Company, Entry, Person
3
+ from wbcore.contrib.directory.models import ClientManagerRelationship, Company, Entry, Person
5
4
  from wbcore.metadata.configs.titles import TitleViewConfig
6
5
 
7
6
 
@@ -29,7 +28,7 @@ class RelationshipTypeModelTitleConfig(TitleViewConfig):
29
28
  class ClientManagerTitleConfig(TitleViewConfig):
30
29
  def get_instance_title(self):
31
30
  if pk := self.view.kwargs.get("pk"):
32
- cmr_request = CMR.objects.get(id=pk)
31
+ cmr_request = ClientManagerRelationship.objects.get(id=pk)
33
32
  return _("Client Manager Relationship Between {client} & {person}").format(
34
33
  client=str(cmr_request.client), person=str(cmr_request.relationship_manager)
35
34
  )
@@ -13,7 +13,6 @@ from wbcore.contrib.documents.models import (
13
13
  class ShareableLinkFilter(wb_filters.FilterSet):
14
14
  valid = wb_filters.BooleanFilter(label=_("Valid"), method="boolean_is_valid")
15
15
  valid_until = wb_filters.DateTimeRangeFilter(
16
- method=wb_filters.DateRangeFilter.base_date_range_filter_method,
17
16
  label="Valid Until",
18
17
  )
19
18
  link = wb_filters.CharFilter(label=_("Link"), method="filter_uuid")
@@ -36,7 +35,6 @@ class ShareableLinkFilter(wb_filters.FilterSet):
36
35
  class ShareableLinkAccessFilter(wb_filters.FilterSet):
37
36
  clearable = (False,)
38
37
  accessed = wb_filters.DateTimeRangeFilter(
39
- method=wb_filters.DateRangeFilter.base_date_range_filter_method,
40
38
  label="Accessed between date",
41
39
  )
42
40
 
@@ -69,7 +69,7 @@ class Role(models.Model):
69
69
  verbose_name_plural = _("Roles")
70
70
 
71
71
 
72
- class SportPerson(ComplexToStringMixin, models.Model):
72
+ class SportPerson(ComplexToStringMixin):
73
73
  roles = models.ManyToManyField(to=Role, blank=True, related_name="sport_persons", verbose_name=_("Roles"))
74
74
  first_name = models.CharField(max_length=255, verbose_name=_("First Name"))
75
75
  last_name = models.CharField(max_length=255, verbose_name=_("Last Name"))
@@ -372,7 +372,7 @@ class Match(ComplexToStringMixin, CalendarItem):
372
372
  WBColor.BLUE_LIGHT.value,
373
373
  WBColor.GREEN_LIGHT.value,
374
374
  ]
375
- return [status for status in zip(cls, colors)]
375
+ return [status for status in zip(cls, colors, strict=False)]
376
376
 
377
377
  home = models.ForeignKey(
378
378
  to="Team",
@@ -432,7 +432,7 @@ class Match(ComplexToStringMixin, CalendarItem):
432
432
 
433
433
  events: models.QuerySet[Event]
434
434
 
435
- def has_permissions(instance: Match, user: User) -> bool:
435
+ def has_permissions(self: Match, user: User) -> bool:
436
436
  if user.is_superuser or user.has_perm("wbcore.change_match_status"):
437
437
  return True
438
438
  return False
@@ -741,7 +741,7 @@ def post_save_team(sender, instance: Team, created: bool, raw: bool, **kwargs):
741
741
  away_match.recompute_computed_str()
742
742
 
743
743
 
744
- class Player(OrderableModel, SportPerson):
744
+ class Player(OrderableModel, SportPerson): # noqa
745
745
  PARTITION_BY = PARENT_FK = "current_team"
746
746
 
747
747
  position = models.CharField(max_length=50, null=True, blank=True, verbose_name=_("Position"))
@@ -108,8 +108,8 @@ class TeamModelSerializer(serializers.ModelSerializer):
108
108
  if email:
109
109
  try:
110
110
  validate_email(email)
111
- except ValidationError:
112
- raise ValidationError({"email": _("Invalid e-mail address")})
111
+ except ValidationError as e:
112
+ raise ValidationError({"email": _("Invalid e-mail address")}) from e
113
113
  if phone_number:
114
114
  try:
115
115
  if phone_number.startswith("00"):
@@ -120,8 +120,8 @@ class TeamModelSerializer(serializers.ModelSerializer):
120
120
  if parser_number:
121
121
  formatted_number = phonenumbers.format_number(parser_number, phonenumbers.PhoneNumberFormat.E164)
122
122
  data["phone_number"] = formatted_number
123
- except Exception:
124
- raise ValidationError({"phone_number": _("Invalid phone number format")})
123
+ except Exception as e:
124
+ raise ValidationError({"phone_number": _("Invalid phone number format")}) from e
125
125
 
126
126
  return super().validate(data)
127
127
 
@@ -25,7 +25,7 @@ USER_PASSWORD = "User_Password"
25
25
  class TestTeam:
26
26
  def test_create_edit_delete_team(self, live_server, selenium):
27
27
  # Creating a test user and login to the WB
28
- user: User = SuperUserFactory(plaintext_password=USER_PASSWORD)
28
+ user: User = SuperUserFactory(plaintext_password=USER_PASSWORD) # noqa
29
29
  actions = ActionChains(selenium, 1000)
30
30
  set_up(selenium, live_server, user.email, USER_PASSWORD)
31
31
 
@@ -97,4 +97,4 @@ class TestViewsets:
97
97
  continent.refresh_from_db()
98
98
  # Assert
99
99
  assert response.status_code == status.HTTP_200_OK
100
- assert getattr(continent, "name") == new_field_data
100
+ assert continent.name == new_field_data
@@ -4,7 +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): ...
7
+ def __init__(self): ... # noqa
8
8
 
9
9
  PermissionObjectModelMixin.__init__ = __init__
10
10
  PermissionObjectModelMixin.objects = mocker.Mock()
@@ -21,8 +21,8 @@ class TestPermissionObjectModelMixin:
21
21
  def test_save_run_assign_permissions(self, mocker, mocked_permission_object_model_mixin):
22
22
  mocker.patch("django.db.models.Model.save")
23
23
  on_commit = mocker.patch("wbcore.contrib.guardian.models.mixins.transaction.on_commit")
24
- ContentType = mocker.patch("wbcore.contrib.guardian.models.mixins.ContentType")
25
- 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()
26
26
 
27
27
  mocked_permission_object_model_mixin().save()
28
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
 
@@ -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"
@@ -8,6 +8,14 @@ class DeserializationError(Exception):
8
8
  pass
9
9
 
10
10
 
11
+ class SkipImportError(Exception):
12
+ """
13
+ Exception excepted when a deserialized skip happens during the process object phase in the Handler.
14
+
15
+ This exception won't stop the importing process and won't trigger a warning
16
+ """
17
+
18
+
11
19
  class ImportError(Exception):
12
20
  """
13
21
  Exception returns when something wrong happens during the processing of the import source and means that not all data were succesfully imported
@@ -35,7 +35,7 @@ class DataBackend(AbstractDataBackend):
35
35
  self.url = url
36
36
 
37
37
  @classmethod
38
- def _check_content_type(self, output: BytesIO, filename: str) -> bool:
38
+ def _check_content_type(cls, output: BytesIO, filename: str) -> bool:
39
39
  """
40
40
  Check if given bytes stream matches the corresponding filename extension
41
41
 
@@ -80,7 +80,7 @@ class DataBackend(AbstractDataBackend):
80
80
  """
81
81
  with suppress(requests.ConnectionError):
82
82
  headers = kwargs.get("headers", {})
83
- r = requests.get(self.url, headers=headers)
83
+ r = requests.get(self.url, headers=headers, timeout=10)
84
84
  if r.ok and (content := r.content):
85
85
  content_file = BytesIO()
86
86
  content_file.write(content)
@@ -12,7 +12,7 @@ from django.db import models
12
12
  from django.db.models import Model
13
13
  from tqdm import tqdm
14
14
 
15
- from .exceptions import DeserializationError, ImportError
15
+ from .exceptions import DeserializationError, ImportError, SkipImportError
16
16
  from .models import ImportedObjectProviderRelationship, ImportSource
17
17
  from .utils import nest_row
18
18
 
@@ -30,6 +30,7 @@ class ImportExportHandler:
30
30
  def __init__(self, import_source: ImportSource, **kwargs):
31
31
  self.import_source: ImportSource = import_source
32
32
  self.model: Type[Model] = apps.get_model(self.MODEL_APP_LABEL)
33
+ self.processed_ids: list[int] = []
33
34
 
34
35
  def _inject_internal_id_from_data(self, data: dict[str, Any]) -> tuple[int, ContentType] | None:
35
36
  if provider_id := data.pop("provider_id", None):
@@ -45,7 +46,7 @@ class ImportExportHandler:
45
46
  else:
46
47
  return provider_id, content_type
47
48
 
48
- def _model_dict_diff(self, model: Any, data: Dict[str, Any]) -> Dict[str, Any]:
49
+ def _model_dict_diff(self, model: Any, data: Dict[str, Any]) -> Dict[str, Any]: # noqa: C901
49
50
  """
50
51
  Given a model and its dictionary representation, compare them and find out the fields that are different
51
52
  Args:
@@ -167,7 +168,7 @@ class ImportExportHandler:
167
168
  error_msg = f"\nError {e} while saving data {change_data} for object id {_object.pk}"
168
169
  self.import_source.log += error_msg
169
170
  if not getattr(self, "allow_update_save_failure", False):
170
- raise ImportError(error_msg)
171
+ raise ImportError(error_msg) from e
171
172
  return True
172
173
  return False
173
174
 
@@ -295,10 +296,14 @@ class ImportExportHandler:
295
296
  unmodified_objs.append(_object)
296
297
  if (
297
298
  len(self.import_source.log) > self.MAX_ALLOWED_LOG_SIZE
298
- ): # In case we are exceedning the max log size, we reset the log to avoid issue when saving it
299
+ ): # In case we are exceeding the max log size, we reset the log to avoid issue when saving it
299
300
  self.import_source.log = ""
301
+ if object_id := getattr(_object, "id", None):
302
+ self.processed_ids.append(object_id)
303
+ except SkipImportError as e:
304
+ self.import_source.log += f"skipping Row {self.import_source.progress_index}: {str(e)}\n"
300
305
  except DeserializationError as e:
301
- self.import_source.errors_log += f"Row {self.import_source.progress_index}: {str(e)}\n"
306
+ self.import_source.errors_log += f"Warning Row {self.import_source.progress_index}: {str(e)}\n"
302
307
  self.import_source.progress_index += 1
303
308
  if with_post_processing:
304
309
  if history.exists():
@@ -95,19 +95,19 @@ class ParserHandler(models.Model):
95
95
  verbose_name_plural = _("Parsers-Handlers")
96
96
 
97
97
  @classmethod
98
- def get_representation_value_key(self) -> str:
98
+ def get_representation_value_key(cls) -> str:
99
99
  return "id"
100
100
 
101
101
  @classmethod
102
- def get_representation_label_key(self) -> str:
102
+ def get_representation_label_key(cls) -> str:
103
103
  return "{{parser}}::{{handler}}"
104
104
 
105
105
  @classmethod
106
- def get_representation_endpoint(self) -> str:
106
+ def get_representation_endpoint(cls) -> str:
107
107
  return "wbcore:io:parserhandlerrepresentation-list"
108
108
 
109
109
 
110
- class ImportedObjectProviderRelationship(ComplexToStringMixin, models.Model):
110
+ class ImportedObjectProviderRelationship(ComplexToStringMixin):
111
111
  """
112
112
  A model that represent the relationship/link between the imported object and a provider.
113
113
 
@@ -218,15 +218,15 @@ class DataBackend(models.Model):
218
218
  return self.title
219
219
 
220
220
  @classmethod
221
- def get_representation_value_key(self) -> str:
221
+ def get_representation_value_key(cls) -> str:
222
222
  return "id"
223
223
 
224
224
  @classmethod
225
- def get_representation_label_key(self) -> str:
225
+ def get_representation_label_key(cls) -> str:
226
226
  return "{{title}}"
227
227
 
228
228
  @classmethod
229
- def get_representation_endpoint(self) -> str:
229
+ def get_representation_endpoint(cls) -> str:
230
230
  return "wbcore:io:databackendrepresentation-list"
231
231
 
232
232
 
@@ -522,7 +522,7 @@ class Source(models.Model):
522
522
  return res
523
523
 
524
524
  @classmethod
525
- def load_sources_from_settings(self, settings: list[tuple[list[tuple[str, str]], str, dict[str, Any]]]):
525
+ def load_sources_from_settings(cls, settings: list[tuple[list[tuple[str, str]], str, dict[str, Any]]]):
526
526
  """
527
527
  Utility classmethod to parser sources from the settings.
528
528
 
@@ -588,15 +588,15 @@ class Source(models.Model):
588
588
  source.save()
589
589
 
590
590
  @classmethod
591
- def get_representation_value_key(self) -> str:
591
+ def get_representation_value_key(cls) -> str:
592
592
  return "id"
593
593
 
594
594
  @classmethod
595
- def get_representation_label_key(self) -> str:
595
+ def get_representation_label_key(cls) -> str:
596
596
  return "{{title}} ({{id}})"
597
597
 
598
598
  @classmethod
599
- def get_representation_endpoint(self) -> str:
599
+ def get_representation_endpoint(cls) -> str:
600
600
  return "wbcore:io:sourcerepresentation-list"
601
601
 
602
602
 
@@ -713,6 +713,9 @@ class ExportSource(ImportExportSource):
713
713
  )
714
714
  ]
715
715
 
716
+ def __str__(self) -> str:
717
+ return str(self.id)
718
+
716
719
  @property
717
720
  def file_format(self):
718
721
  return get_django_import_export_format(self.format)()
@@ -915,15 +918,15 @@ class ImportSource(ImportExportSource):
915
918
  )
916
919
 
917
920
  @classmethod
918
- def get_representation_value_key(self) -> str:
921
+ def get_representation_value_key(cls) -> str:
919
922
  return "id"
920
923
 
921
924
  @classmethod
922
- def get_representation_label_key(self) -> str:
925
+ def get_representation_label_key(cls) -> str:
923
926
  return "{{file}}"
924
927
 
925
928
  @classmethod
926
- def get_representation_endpoint(self) -> str:
929
+ def get_representation_endpoint(cls) -> str:
927
930
  return "wbcore:io:importsourcerepresentation-list"
928
931
 
929
932
 
@@ -88,8 +88,8 @@ class ImportSourceModelSerializer(ModelSerializer):
88
88
  parser_lookup = dict(id=parser_handler)
89
89
  try:
90
90
  data["parser_handler"] = ParserHandler.objects.get(**parser_lookup)
91
- except (ParserHandler.DoesNotExist, ValueError):
92
- raise ValidationError({"parser_handler": "Invalid parser handler"})
91
+ except (ParserHandler.DoesNotExist, ValueError) as e:
92
+ raise ValidationError({"parser_handler": "Invalid parser handler"}) from e
93
93
  return data
94
94
 
95
95
  def create(self, validated_data):