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.
- wbcore/cache/decorators.py +3 -3
- wbcore/cache/registry.py +3 -2
- wbcore/configs/decorators.py +1 -1
- wbcore/configurations/configurations/apps.py +2 -2
- wbcore/configurations/configurations/authentication.py +1 -1
- wbcore/configurations/configurations/base.py +1 -1
- wbcore/configurations/configurations/cache.py +1 -1
- wbcore/configurations/configurations/maintenance.py +1 -1
- wbcore/configurations/configurations/media.py +1 -1
- wbcore/configurations/configurations/middleware.py +1 -1
- wbcore/configurations/configurations/rest_framework.py +1 -1
- wbcore/configurations/configurations/static.py +1 -1
- wbcore/configurations/configurations/wbcore.py +1 -1
- wbcore/content_type/serializers.py +1 -1
- wbcore/content_type/utils.py +3 -3
- wbcore/contrib/agenda/viewsets/calendar_items.py +7 -7
- wbcore/contrib/ai/llm/config.py +1 -1
- wbcore/contrib/authentication/admin.py +2 -2
- wbcore/contrib/authentication/filters.py +0 -1
- wbcore/contrib/authentication/models/users.py +3 -3
- wbcore/contrib/authentication/models/users_activities.py +1 -1
- wbcore/contrib/authentication/serializers/users.py +2 -2
- wbcore/contrib/authentication/tests/test_tokens.py +3 -3
- wbcore/contrib/authentication/tests/test_users.py +0 -1
- wbcore/contrib/authentication/viewsets/user_activities.py +2 -1
- wbcore/contrib/authentication/viewsets/users.py +6 -4
- wbcore/contrib/color/models.py +2 -1
- wbcore/contrib/currency/factories.py +1 -1
- wbcore/contrib/currency/import_export/backends/fixerio/currency_fx_rates.py +3 -1
- wbcore/contrib/currency/models.py +28 -8
- wbcore/contrib/currency/serializers.py +5 -1
- wbcore/contrib/currency/tests/test_serializers.py +7 -3
- wbcore/contrib/currency/tests/test_viewsets.py +1 -1
- wbcore/contrib/currency/viewsets/currency.py +2 -2
- wbcore/contrib/dataloader/utils.py +2 -2
- wbcore/contrib/directory/factories/__init__.py +1 -1
- wbcore/contrib/directory/factories/entries.py +1 -1
- wbcore/contrib/directory/models/contacts.py +2 -2
- wbcore/contrib/directory/models/entries.py +18 -4
- wbcore/contrib/directory/models/relationships.py +25 -30
- wbcore/contrib/directory/permissions.py +6 -0
- wbcore/contrib/directory/serializers/companies.py +15 -8
- wbcore/contrib/directory/serializers/contacts.py +8 -8
- wbcore/contrib/directory/serializers/entries.py +24 -15
- wbcore/contrib/directory/serializers/entry_representations.py +4 -2
- wbcore/contrib/directory/serializers/persons.py +8 -9
- wbcore/contrib/directory/serializers/relationships.py +2 -2
- wbcore/contrib/directory/tests/conftest.py +2 -0
- wbcore/contrib/directory/tests/disable_signals.py +11 -1
- wbcore/contrib/directory/tests/signals.py +2 -2
- wbcore/contrib/directory/tests/test_models.py +88 -66
- wbcore/contrib/directory/tests/test_serializers.py +1 -1
- wbcore/contrib/directory/tests/test_viewsets.py +8 -8
- wbcore/contrib/directory/viewsets/buttons/__init__.py +1 -1
- wbcore/contrib/directory/viewsets/buttons/relationships.py +32 -0
- wbcore/contrib/directory/viewsets/contacts.py +6 -6
- wbcore/contrib/directory/viewsets/display/__init__.py +1 -1
- wbcore/contrib/directory/viewsets/display/entries.py +51 -36
- wbcore/contrib/directory/viewsets/display/relationships.py +22 -22
- wbcore/contrib/directory/viewsets/entries.py +4 -5
- wbcore/contrib/directory/viewsets/previews/entries.py +3 -3
- wbcore/contrib/directory/viewsets/relationships.py +16 -2
- wbcore/contrib/directory/viewsets/titles/relationships.py +2 -3
- wbcore/contrib/documents/filters.py +0 -2
- wbcore/contrib/example_app/models.py +4 -4
- wbcore/contrib/example_app/serializers/person_team.py +4 -4
- wbcore/contrib/example_app/tests/e2e/test_teams.py +1 -1
- wbcore/contrib/geography/tests/test_viewsets.py +1 -1
- wbcore/contrib/guardian/tests/test_model_mixins.py +3 -3
- wbcore/contrib/guardian/tests/test_tasks.py +9 -9
- wbcore/contrib/guardian/tests/test_viewsets.py +2 -2
- wbcore/contrib/icons/backends/default.py +1 -0
- wbcore/contrib/icons/backends/material.py +1 -0
- wbcore/contrib/icons/icons.py +5 -8
- wbcore/contrib/io/exceptions.py +8 -0
- wbcore/contrib/io/import_export/backends/stream.py +2 -2
- wbcore/contrib/io/imports.py +10 -5
- wbcore/contrib/io/models.py +17 -14
- wbcore/contrib/io/serializers.py +2 -2
- wbcore/contrib/io/tests/test_backends.py +1 -1
- wbcore/contrib/io/tests/test_imports.py +1 -1
- wbcore/contrib/io/viewset_mixins.py +4 -4
- wbcore/contrib/notifications/dispatch.py +18 -7
- wbcore/contrib/pandas/filterset.py +8 -7
- wbcore/contrib/pandas/views.py +7 -5
- wbcore/contrib/tags/models/tags.py +4 -1
- wbcore/contrib/workflow/factories/display.py +2 -2
- wbcore/contrib/workflow/models/data.py +7 -4
- wbcore/contrib/workflow/models/process.py +2 -2
- wbcore/contrib/workflow/serializers/data.py +8 -8
- wbcore/contrib/workflow/tests/test_models/test_condition.py +1 -1
- wbcore/contrib/workflow/workflows/assignees.py +4 -4
- wbcore/dynamic_preferences_registry.py +23 -9
- wbcore/enums.py +2 -1
- wbcore/filters/fields/content_type.py +5 -4
- wbcore/filters/fields/datetime.py +34 -9
- wbcore/filters/fields/models.py +2 -2
- wbcore/filters/filterset.py +22 -6
- wbcore/filters/mixins.py +6 -2
- wbcore/forms.py +6 -6
- wbcore/fsm/markdown_extensions.py +1 -1
- wbcore/fsm/mixins.py +7 -4
- wbcore/markdown/models.py +8 -5
- wbcore/metadata/configs/buttons/bases.py +6 -6
- wbcore/metadata/configs/buttons/buttons.py +2 -1
- wbcore/metadata/configs/buttons/view_config.py +5 -3
- wbcore/metadata/configs/display/display.py +2 -2
- wbcore/metadata/configs/display/formatting.py +6 -7
- wbcore/metadata/configs/display/list_display.py +6 -7
- wbcore/metadata/configs/display/models.py +6 -0
- wbcore/metadata/configs/fields.py +6 -1
- wbcore/metadata/configs/filter_fields.py +12 -11
- wbcore/models/fields.py +2 -2
- wbcore/permissions/permissions.py +2 -2
- wbcore/permissions/utils.py +2 -2
- wbcore/reversion/viewsets/titles.py +4 -3
- wbcore/serializers/__init__.py +1 -0
- wbcore/serializers/fields/__init__.py +1 -0
- wbcore/serializers/fields/datetime.py +35 -6
- wbcore/serializers/fields/fields.py +1 -1
- wbcore/serializers/fields/fsm.py +1 -1
- wbcore/serializers/fields/list.py +1 -1
- wbcore/serializers/fields/mixins.py +13 -5
- wbcore/serializers/fields/related.py +4 -6
- wbcore/serializers/fields/text.py +1 -1
- wbcore/serializers/fields/types.py +1 -0
- wbcore/serializers/serializers.py +6 -2
- wbcore/tasks.py +2 -2
- wbcore/templates/wbcore/email_base_template.html +3 -3
- wbcore/test/e2e_helpers_methods/e2e_checks.py +10 -4
- wbcore/test/e2e_helpers_methods/e2e_helper_methods.py +4 -2
- wbcore/test/mixins.py +1 -1
- wbcore/test/tests.py +6 -9
- wbcore/test/utils.py +3 -4
- wbcore/tests/e2e/test_e2e.py +2 -2
- wbcore/tests/test_cache/test_decorators.py +3 -3
- wbcore/tests/test_configs.py +1 -1
- wbcore/tests/test_fields/test_number_fields.py +1 -1
- wbcore/tests/test_filters/test_mixins.py +3 -3
- wbcore/tests/test_models/test_mixins.py +1 -1
- wbcore/tests/test_utils/test_date.py +1 -1
- wbcore/tests/test_utils/test_date_builder.py +25 -1
- wbcore/utils/date.py +18 -2
- wbcore/utils/figures.py +2 -2
- wbcore/utils/models.py +3 -2
- wbcore/utils/reportlab.py +7 -0
- wbcore/utils/rrules.py +1 -1
- wbcore/utils/string_loader.py +1 -1
- wbcore/utils/strings.py +2 -2
- wbcore/viewsets/mixins.py +6 -4
- {wbcore-1.54.10.dist-info → wbcore-1.58.2.dist-info}/METADATA +2 -1
- {wbcore-1.54.10.dist-info → wbcore-1.58.2.dist-info}/RECORD +153 -151
- {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
|
|
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
|
|
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=
|
|
207
|
-
label=
|
|
208
|
-
value=
|
|
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=
|
|
212
|
-
label=
|
|
213
|
-
value=
|
|
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=
|
|
217
|
-
label=
|
|
218
|
-
value=
|
|
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=
|
|
222
|
-
label=
|
|
223
|
-
value=
|
|
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=
|
|
227
|
-
label=
|
|
228
|
-
value=
|
|
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=("==",
|
|
245
|
+
condition=("==", ClientManagerRelationship.Status.PENDINGADD.name),
|
|
246
246
|
),
|
|
247
247
|
dp.FormattingRule(
|
|
248
248
|
style={"backgroundColor": WBColor.YELLOW_DARK.value},
|
|
249
|
-
condition=("==",
|
|
249
|
+
condition=("==", ClientManagerRelationship.Status.PENDINGREMOVE.name),
|
|
250
250
|
),
|
|
251
251
|
dp.FormattingRule(
|
|
252
252
|
style={"backgroundColor": WBColor.GREEN_LIGHT.value},
|
|
253
|
-
condition=("==",
|
|
253
|
+
condition=("==", ClientManagerRelationship.Status.APPROVED.name),
|
|
254
254
|
),
|
|
255
255
|
dp.FormattingRule(
|
|
256
256
|
style={"backgroundColor": WBColor.RED_LIGHT.value},
|
|
257
|
-
condition=("==",
|
|
257
|
+
condition=("==", ClientManagerRelationship.Status.DRAFT.name),
|
|
258
258
|
),
|
|
259
259
|
dp.FormattingRule(
|
|
260
260
|
style={"backgroundColor": WBColor.GREY.value},
|
|
261
|
-
condition=("==",
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
.
|
|
156
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
|
@@ -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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
+
user_class = mocker.patch("wbcore.contrib.guardian.models.mixins.User")
|
|
67
67
|
user = mocker.Mock()
|
|
68
68
|
|
|
69
|
-
|
|
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 = [
|
|
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
|
|
|
@@ -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"
|
wbcore/contrib/icons/icons.py
CHANGED
|
@@ -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__(
|
|
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__(
|
|
22
|
+
cls = super().__new__(cls, classname, bases, classdict, **kwds)
|
|
23
23
|
with suppress(ModuleNotFoundError):
|
|
24
|
-
|
|
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"
|
wbcore/contrib/io/exceptions.py
CHANGED
|
@@ -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(
|
|
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)
|
wbcore/contrib/io/imports.py
CHANGED
|
@@ -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
|
|
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():
|
wbcore/contrib/io/models.py
CHANGED
|
@@ -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(
|
|
98
|
+
def get_representation_value_key(cls) -> str:
|
|
99
99
|
return "id"
|
|
100
100
|
|
|
101
101
|
@classmethod
|
|
102
|
-
def get_representation_label_key(
|
|
102
|
+
def get_representation_label_key(cls) -> str:
|
|
103
103
|
return "{{parser}}::{{handler}}"
|
|
104
104
|
|
|
105
105
|
@classmethod
|
|
106
|
-
def get_representation_endpoint(
|
|
106
|
+
def get_representation_endpoint(cls) -> str:
|
|
107
107
|
return "wbcore:io:parserhandlerrepresentation-list"
|
|
108
108
|
|
|
109
109
|
|
|
110
|
-
class ImportedObjectProviderRelationship(ComplexToStringMixin
|
|
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(
|
|
221
|
+
def get_representation_value_key(cls) -> str:
|
|
222
222
|
return "id"
|
|
223
223
|
|
|
224
224
|
@classmethod
|
|
225
|
-
def get_representation_label_key(
|
|
225
|
+
def get_representation_label_key(cls) -> str:
|
|
226
226
|
return "{{title}}"
|
|
227
227
|
|
|
228
228
|
@classmethod
|
|
229
|
-
def get_representation_endpoint(
|
|
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(
|
|
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(
|
|
591
|
+
def get_representation_value_key(cls) -> str:
|
|
592
592
|
return "id"
|
|
593
593
|
|
|
594
594
|
@classmethod
|
|
595
|
-
def get_representation_label_key(
|
|
595
|
+
def get_representation_label_key(cls) -> str:
|
|
596
596
|
return "{{title}} ({{id}})"
|
|
597
597
|
|
|
598
598
|
@classmethod
|
|
599
|
-
def get_representation_endpoint(
|
|
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(
|
|
921
|
+
def get_representation_value_key(cls) -> str:
|
|
919
922
|
return "id"
|
|
920
923
|
|
|
921
924
|
@classmethod
|
|
922
|
-
def get_representation_label_key(
|
|
925
|
+
def get_representation_label_key(cls) -> str:
|
|
923
926
|
return "{{file}}"
|
|
924
927
|
|
|
925
928
|
@classmethod
|
|
926
|
-
def get_representation_endpoint(
|
|
929
|
+
def get_representation_endpoint(cls) -> str:
|
|
927
930
|
return "wbcore:io:importsourcerepresentation-list"
|
|
928
931
|
|
|
929
932
|
|
wbcore/contrib/io/serializers.py
CHANGED
|
@@ -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):
|