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
|
@@ -117,7 +117,7 @@ class TestBackend:
|
|
|
117
117
|
with sftpserver.serve_content(VFS):
|
|
118
118
|
import_credential = import_credential_factory.create(
|
|
119
119
|
username="user",
|
|
120
|
-
password="password",
|
|
120
|
+
password="password", # noqa
|
|
121
121
|
additional_resources={
|
|
122
122
|
"host": sftpserver.host,
|
|
123
123
|
"port": sftpserver.port,
|
|
@@ -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(
|
|
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):
|
|
@@ -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):
|
|
@@ -84,12 +84,12 @@ class ImportExportDRFMixin(ImportExportMixin):
|
|
|
84
84
|
with suppress(AttributeError, AssertionError):
|
|
85
85
|
# we have to mocky patch the action to be "list" because sometime we differentiate the serializer to use
|
|
86
86
|
previous_action = getattr(self, "action", "list")
|
|
87
|
-
|
|
87
|
+
self.action = "list"
|
|
88
88
|
serializer_class = getattr(
|
|
89
89
|
self, "serializer_class", self.get_serializer_class()
|
|
90
90
|
) # we prioritize the default serializer class attribute
|
|
91
91
|
resource_kwargs["serializer_class_path"] = serializer_class.__module__ + "." + serializer_class.__name__
|
|
92
|
-
|
|
92
|
+
self.action = previous_action
|
|
93
93
|
return resource_kwargs
|
|
94
94
|
|
|
95
95
|
def _get_data_for_export(self, request, queryset, *args, **kwargs) -> tablib.Dataset:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from typing import Iterable
|
|
2
2
|
|
|
3
|
+
from celery import shared_task
|
|
3
4
|
from django.conf import settings
|
|
4
5
|
from django.contrib.auth import get_user_model
|
|
5
6
|
from django.db import transaction
|
|
@@ -43,22 +44,32 @@ def send_notification(
|
|
|
43
44
|
if isinstance(users, User):
|
|
44
45
|
users = [users]
|
|
45
46
|
for user in users:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
user
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
notification_type = NotificationType.objects.get(code=code)
|
|
48
|
+
if (
|
|
49
|
+
user.is_active
|
|
50
|
+
and NotificationTypeSetting.objects.filter(notification_type=notification_type, user=user).exists()
|
|
51
|
+
):
|
|
51
52
|
if not endpoint:
|
|
52
53
|
endpoint = reverse(reverse_name, reverse_args, reverse_kwargs) if reverse_name else None
|
|
53
54
|
notification = Notification.objects.create(
|
|
54
55
|
title=title,
|
|
55
56
|
body=body,
|
|
56
57
|
user=user,
|
|
57
|
-
notification_type=
|
|
58
|
+
notification_type=notification_type,
|
|
58
59
|
endpoint=endpoint,
|
|
59
60
|
sent=timezone.now(),
|
|
60
61
|
)
|
|
61
|
-
transaction.on_commit(
|
|
62
|
+
transaction.on_commit(
|
|
63
|
+
lambda notification_pk=notification.pk: send_notification_task.delay(notification_pk)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@shared_task()
|
|
68
|
+
def send_notification_as_task(code, title, body, user_id, **kwargs):
|
|
69
|
+
if not isinstance(user_id, list):
|
|
70
|
+
user_id = [user_id]
|
|
71
|
+
user = User.objects.filter(id__in=user_id)
|
|
72
|
+
send_notification(code, title, body, user, **kwargs)
|
|
62
73
|
|
|
63
74
|
|
|
64
75
|
@receiver(handle_widget_sharing)
|
|
@@ -16,12 +16,13 @@ class PandasFilterSetMixin:
|
|
|
16
16
|
queryset = self.filters[name].filter(queryset, value)
|
|
17
17
|
except FieldError:
|
|
18
18
|
pass
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
if not isinstance(queryset, models.QuerySet):
|
|
20
|
+
raise AssertionError(
|
|
21
|
+
"Expected '%s.%s' to return a QuerySet, but got a %s instead."
|
|
22
|
+
% (
|
|
23
|
+
type(self).__name__,
|
|
24
|
+
name,
|
|
25
|
+
type(queryset).__name__,
|
|
26
|
+
)
|
|
25
27
|
)
|
|
26
|
-
)
|
|
27
28
|
return queryset
|
wbcore/contrib/pandas/views.py
CHANGED
|
@@ -40,8 +40,8 @@ class PandasMixin(CacheMixin):
|
|
|
40
40
|
@cached_property
|
|
41
41
|
def df(self) -> pd.DataFrame:
|
|
42
42
|
if not hasattr(self, "_df"):
|
|
43
|
-
|
|
44
|
-
return
|
|
43
|
+
self._df = self._get_dataframe()
|
|
44
|
+
return self._df
|
|
45
45
|
|
|
46
46
|
# BASIC DATAFRAME GENERATION FRAMEWORK METHODS
|
|
47
47
|
def filter_queryset(self, queryset: QuerySet) -> QuerySet:
|
|
@@ -80,11 +80,13 @@ class PandasMixin(CacheMixin):
|
|
|
80
80
|
)
|
|
81
81
|
|
|
82
82
|
def get_queryset(self):
|
|
83
|
-
|
|
83
|
+
if not hasattr(self, "queryset"):
|
|
84
|
+
raise AssertionError("Either specify a queryset or implement the get_queryset method.")
|
|
84
85
|
return self.queryset
|
|
85
86
|
|
|
86
87
|
def get_dataframe(self, request, queryset, **kwargs):
|
|
87
|
-
|
|
88
|
+
if not self.get_pandas_fields(request):
|
|
89
|
+
raise AssertionError("No pandas_fields specified")
|
|
88
90
|
return pd.DataFrame(queryset.values(*self.get_pandas_fields(request).to_dict().keys()))
|
|
89
91
|
|
|
90
92
|
def manipulate_dataframe(self, df):
|
|
@@ -110,7 +112,7 @@ class PandasMixin(CacheMixin):
|
|
|
110
112
|
df = pd.DataFrame(
|
|
111
113
|
columns=[field.key for field in self.get_pandas_fields(self.request).fields]
|
|
112
114
|
) # if queryset is empty, we make sure the returning df contains all the columns to avoid keyerrors exception
|
|
113
|
-
|
|
115
|
+
self._df = df
|
|
114
116
|
df = self.filter_dataframe(df, **kwargs)
|
|
115
117
|
return df
|
|
116
118
|
|
|
@@ -17,7 +17,7 @@ class ManagedMixin(models.Model):
|
|
|
17
17
|
abstract = True
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
class Tag(
|
|
20
|
+
class Tag(ComplexToStringMixin, ManagedMixin):
|
|
21
21
|
title = models.CharField(max_length=255)
|
|
22
22
|
|
|
23
23
|
slug = models.CharField(max_length=255, null=True, blank=True)
|
|
@@ -35,6 +35,9 @@ class Tag(ManagedMixin, ComplexToStringMixin, WBModel):
|
|
|
35
35
|
verbose_name = "Tag"
|
|
36
36
|
verbose_name_plural = "Tags"
|
|
37
37
|
|
|
38
|
+
def __str__(self) -> str:
|
|
39
|
+
return super().__str__()
|
|
40
|
+
|
|
38
41
|
@classmethod
|
|
39
42
|
def get_endpoint_basename(cls):
|
|
40
43
|
return "wbcore:tags:tag"
|
|
@@ -12,9 +12,9 @@ fake = Faker()
|
|
|
12
12
|
def _generate_grid_areas() -> list[list[str]]:
|
|
13
13
|
outer_list = []
|
|
14
14
|
inner_list_length = random.randint(1, 5)
|
|
15
|
-
for
|
|
15
|
+
for _ in range(random.randint(1, 5)):
|
|
16
16
|
inner_list = []
|
|
17
|
-
for
|
|
17
|
+
for _ in range(inner_list_length):
|
|
18
18
|
inner_list.append(random.choice(PersonModelSerializer.Meta.fields))
|
|
19
19
|
outer_list.append(inner_list)
|
|
20
20
|
return outer_list
|
|
@@ -28,7 +28,10 @@ class Data(WBModel):
|
|
|
28
28
|
serializers.DateTimeField(),
|
|
29
29
|
serializers.BooleanField(),
|
|
30
30
|
]
|
|
31
|
-
return {
|
|
31
|
+
return {
|
|
32
|
+
data_type: serializer_field
|
|
33
|
+
for data_type, serializer_field in zip(cls, serializer_fields, strict=False)
|
|
34
|
+
}
|
|
32
35
|
|
|
33
36
|
@classmethod
|
|
34
37
|
def get_cast_mapping(cls) -> dict:
|
|
@@ -39,7 +42,7 @@ class Data(WBModel):
|
|
|
39
42
|
datetime.strptime,
|
|
40
43
|
bool,
|
|
41
44
|
]
|
|
42
|
-
return {data_type: cast_callable for data_type, cast_callable in zip(cls, cast_callables)}
|
|
45
|
+
return {data_type: cast_callable for data_type, cast_callable in zip(cls, cast_callables, strict=False)}
|
|
43
46
|
|
|
44
47
|
workflow = models.ForeignKey(
|
|
45
48
|
to="workflow.Workflow",
|
|
@@ -176,8 +179,8 @@ class Data(WBModel):
|
|
|
176
179
|
format_str = "%d.%m.%Y %H:%M:%S"
|
|
177
180
|
try:
|
|
178
181
|
casted_object = data_object.strftime(format_str)
|
|
179
|
-
except AttributeError:
|
|
180
|
-
raise ValueError(gettext("Date(time) type selected but no date(time) object provided!"))
|
|
182
|
+
except AttributeError as e:
|
|
183
|
+
raise ValueError(gettext("Date(time) type selected but no date(time) object provided!")) from e
|
|
181
184
|
|
|
182
185
|
elif data_type == cls.DataType.BOOL:
|
|
183
186
|
if data_object is True:
|
|
@@ -27,7 +27,7 @@ class Process(WBModel):
|
|
|
27
27
|
WBColor.GREEN_LIGHT.value,
|
|
28
28
|
WBColor.RED_LIGHT.value,
|
|
29
29
|
]
|
|
30
|
-
return [choice for choice in zip(cls, colors)]
|
|
30
|
+
return [choice for choice in zip(cls, colors, strict=False)]
|
|
31
31
|
|
|
32
32
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, verbose_name=_("UUID"))
|
|
33
33
|
workflow = models.ForeignKey(
|
|
@@ -124,7 +124,7 @@ class ProcessStep(WBModel):
|
|
|
124
124
|
WBColor.RED_LIGHT.value,
|
|
125
125
|
WBColor.GREY.value,
|
|
126
126
|
]
|
|
127
|
-
return [choice for choice in zip(cls, colors)]
|
|
127
|
+
return [choice for choice in zip(cls, colors, strict=False)]
|
|
128
128
|
|
|
129
129
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False, verbose_name=_("UUID"))
|
|
130
130
|
process = models.ForeignKey(
|
|
@@ -43,7 +43,7 @@ class DataModelSerializer(wb_serializers.ModelSerializer):
|
|
|
43
43
|
if data_type:
|
|
44
44
|
try:
|
|
45
45
|
Data.cast_value_to_datatype(data_type, default)
|
|
46
|
-
except ValueError:
|
|
46
|
+
except ValueError as e:
|
|
47
47
|
if data_type == Data.DataType.DATE:
|
|
48
48
|
raise ValidationError(
|
|
49
49
|
{
|
|
@@ -51,7 +51,7 @@ class DataModelSerializer(wb_serializers.ModelSerializer):
|
|
|
51
51
|
"Invalid default value for this data type. Please use a date formatted to 'day.month.year'."
|
|
52
52
|
)
|
|
53
53
|
}
|
|
54
|
-
)
|
|
54
|
+
) from None
|
|
55
55
|
elif data_type == Data.DataType.DATETIME:
|
|
56
56
|
raise ValidationError(
|
|
57
57
|
{
|
|
@@ -59,8 +59,8 @@ class DataModelSerializer(wb_serializers.ModelSerializer):
|
|
|
59
59
|
"Invalid default value for this data type. Please use a datetime formatted to 'day.month.year hour:minute:second' in the 24h format."
|
|
60
60
|
)
|
|
61
61
|
}
|
|
62
|
-
)
|
|
63
|
-
raise ValidationError({"default": _("Invalid default value for this data type.")})
|
|
62
|
+
) from None
|
|
63
|
+
raise ValidationError({"default": _("Invalid default value for this data type.")}) from e
|
|
64
64
|
|
|
65
65
|
return data
|
|
66
66
|
|
|
@@ -99,7 +99,7 @@ class DataValueModelSerializer(wb_serializers.ModelSerializer):
|
|
|
99
99
|
if data_obj and value:
|
|
100
100
|
try:
|
|
101
101
|
Data.cast_value_to_datatype(data_obj.data_type, value)
|
|
102
|
-
except ValueError:
|
|
102
|
+
except ValueError as e:
|
|
103
103
|
if data_obj.data_type == Data.DataType.DATE:
|
|
104
104
|
raise ValidationError(
|
|
105
105
|
{
|
|
@@ -107,7 +107,7 @@ class DataValueModelSerializer(wb_serializers.ModelSerializer):
|
|
|
107
107
|
"Invalid value for this data type. Please use a date formatted to 'day.month.year'."
|
|
108
108
|
)
|
|
109
109
|
}
|
|
110
|
-
)
|
|
110
|
+
) from None
|
|
111
111
|
elif data_obj.data_type == Data.DataType.DATETIME:
|
|
112
112
|
raise ValidationError(
|
|
113
113
|
{
|
|
@@ -115,8 +115,8 @@ class DataValueModelSerializer(wb_serializers.ModelSerializer):
|
|
|
115
115
|
"Invalid value for this data type. Please use a datetime formatted to 'day.month.year hour:minute:second' in the 24h format."
|
|
116
116
|
)
|
|
117
117
|
}
|
|
118
|
-
)
|
|
119
|
-
raise ValidationError({"value": _("Invalid value for this data type.")})
|
|
118
|
+
) from None
|
|
119
|
+
raise ValidationError({"value": _("Invalid value for this data type.")}) from e
|
|
120
120
|
|
|
121
121
|
return data
|
|
122
122
|
|
|
@@ -10,7 +10,7 @@ class TestCondition:
|
|
|
10
10
|
def test_errors_satisfied_not_called(self, condition_factory):
|
|
11
11
|
condition = condition_factory()
|
|
12
12
|
with pytest.raises(ValueError):
|
|
13
|
-
condition.errors
|
|
13
|
+
assert condition.errors == []
|
|
14
14
|
|
|
15
15
|
def test_errors(self, condition_factory):
|
|
16
16
|
condition = condition_factory()
|
|
@@ -59,14 +59,14 @@ def weighted_random(process_step: ProcessStep, **kwargs) -> User | None:
|
|
|
59
59
|
# We redistribute each occurrence number between all of the other list items to increase their probability
|
|
60
60
|
redistributed_list: list[int] = [0 for i in range(group_user_count)]
|
|
61
61
|
for index, elem in enumerate(number_of_past_assignee_occurrences):
|
|
62
|
-
for index2
|
|
62
|
+
for index2 in range(len(redistributed_list)):
|
|
63
63
|
if not index2 == index:
|
|
64
64
|
redistributed_list[index2] += elem / (group_user_count - 1) if elem else 0
|
|
65
65
|
# Transform the list of absolute values into percentages
|
|
66
66
|
new_weights: list[float] = [x / sum(redistributed_list) for x in redistributed_list]
|
|
67
|
-
new_assignee: User = choices(group_member_list, weights=new_weights)[0]
|
|
67
|
+
new_assignee: User = choices(group_member_list, weights=new_weights)[0] # noqa
|
|
68
68
|
else:
|
|
69
|
-
new_assignee: User = choices(group_member_list)[0]
|
|
69
|
+
new_assignee: User = choices(group_member_list)[0] # noqa
|
|
70
70
|
return new_assignee
|
|
71
71
|
|
|
72
72
|
process_step.step.get_casted_step().set_failed(
|
|
@@ -79,7 +79,7 @@ def weighted_random(process_step: ProcessStep, **kwargs) -> User | None:
|
|
|
79
79
|
@register_assignee("Random Group Member")
|
|
80
80
|
def random_group_member(process_step: ProcessStep, **kwargs) -> User | None:
|
|
81
81
|
if (group := process_step.group) and group.user_set.exists():
|
|
82
|
-
return group.user_set.all()[randint(0, group.user_set.count() - 1)]
|
|
82
|
+
return group.user_set.all()[randint(0, group.user_set.count() - 1)] # noqa
|
|
83
83
|
|
|
84
84
|
process_step.step.get_casted_step().set_failed(
|
|
85
85
|
process_step,
|
|
@@ -5,6 +5,7 @@ from dynamic_preferences.types import IntegerPreference, StringPreference
|
|
|
5
5
|
from dynamic_preferences.users.registries import user_preferences_registry
|
|
6
6
|
|
|
7
7
|
from wbcore.contrib.dynamic_preferences.types import ChoicePreference, LanguageChoicePreference
|
|
8
|
+
from wbcore.utils.date import get_timezone_choices
|
|
8
9
|
|
|
9
10
|
wbcore = Section("wbcore")
|
|
10
11
|
|
|
@@ -50,8 +51,21 @@ class LanguagePreference(LanguageChoicePreference):
|
|
|
50
51
|
|
|
51
52
|
|
|
52
53
|
@user_preferences_registry.register
|
|
53
|
-
class
|
|
54
|
+
class TimezonePreference(ChoicePreference):
|
|
54
55
|
weight = 0
|
|
56
|
+
# Value is a IANA timezone name
|
|
57
|
+
choices = get_timezone_choices()
|
|
58
|
+
section = wbcore
|
|
59
|
+
name = "timezone"
|
|
60
|
+
default = "Europe/Berlin"
|
|
61
|
+
|
|
62
|
+
verbose_name = _("Timezone")
|
|
63
|
+
help_text = _("Pick the timezone in which you want the workbench's dates to be displayed in.")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@user_preferences_registry.register
|
|
67
|
+
class DateFormatPreference(ChoicePreference):
|
|
68
|
+
weight = 1
|
|
55
69
|
choices = [
|
|
56
70
|
("DD.MM.YYYY", "13.04.2007"),
|
|
57
71
|
("DD/MM/YYYY", "13/04/2007"),
|
|
@@ -77,7 +91,7 @@ class DateFormatPreference(ChoicePreference):
|
|
|
77
91
|
|
|
78
92
|
@user_preferences_registry.register
|
|
79
93
|
class TimeFormatPreference(ChoicePreference):
|
|
80
|
-
weight =
|
|
94
|
+
weight = 2
|
|
81
95
|
choices = [
|
|
82
96
|
("HH:mm", "14:05"),
|
|
83
97
|
("hh:mm", "02:05"),
|
|
@@ -100,18 +114,18 @@ class TimeFormatPreference(ChoicePreference):
|
|
|
100
114
|
|
|
101
115
|
@user_preferences_registry.register
|
|
102
116
|
class NumberFormatPreference(ChoicePreference):
|
|
103
|
-
weight =
|
|
104
|
-
# Value is a BCP 47
|
|
117
|
+
weight = 3
|
|
118
|
+
# Value is a BCP 47 region subtag
|
|
105
119
|
choices = [
|
|
106
|
-
("
|
|
107
|
-
("
|
|
108
|
-
("
|
|
109
|
-
("
|
|
120
|
+
("US", "1,234,567.89"),
|
|
121
|
+
("FR", "1\u202f234\u202f567,89"),
|
|
122
|
+
("DE", "1.234.567,89"),
|
|
123
|
+
("CH", "1’234’567.89"),
|
|
110
124
|
]
|
|
111
125
|
|
|
112
126
|
section = wbcore
|
|
113
127
|
name = "number_format"
|
|
114
|
-
default = "
|
|
128
|
+
default = "US"
|
|
115
129
|
|
|
116
130
|
verbose_name = _("Number Format")
|
|
117
131
|
help_text = _("Choose how you want numbers to appear throughout the Workbench.")
|
wbcore/enums.py
CHANGED
|
@@ -39,7 +39,8 @@ class Unit(Enum):
|
|
|
39
39
|
return (float(_value), self.value)
|
|
40
40
|
|
|
41
41
|
def unit(self, _value: Union[float, str, int]):
|
|
42
|
-
|
|
42
|
+
if not isinstance(_value, (float, str, int)):
|
|
43
|
+
raise AssertionError("_value needs to be one of str, float or int")
|
|
43
44
|
|
|
44
45
|
return f"{float(_value)}{self.value}"
|
|
45
46
|
|
|
@@ -29,16 +29,17 @@ class MultipleChoiceContentTypeFilter(WBCoreFilterMixin, django_filters.Filter):
|
|
|
29
29
|
def filter(self, qs, value):
|
|
30
30
|
if value in EMPTY_VALUES:
|
|
31
31
|
return qs
|
|
32
|
-
|
|
33
32
|
conditions = [
|
|
34
33
|
(
|
|
35
34
|
Q(**{self.content_type_label: ContentType.objects.get_for_model(val)})
|
|
36
35
|
& Q(**{self.object_id_label: val.id})
|
|
37
36
|
)
|
|
38
|
-
for val in value
|
|
37
|
+
for val in filter(None, value)
|
|
39
38
|
]
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
if conditions:
|
|
40
|
+
qs = qs.filter(reduce(operator.or_, conditions))
|
|
41
|
+
else:
|
|
42
|
+
qs = qs.none()
|
|
42
43
|
if self.distinct:
|
|
43
44
|
qs = qs.distinct()
|
|
44
45
|
return qs
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
from contextlib import suppress
|
|
2
2
|
|
|
3
3
|
import django_filters
|
|
4
|
+
from django.contrib.postgres.fields import RangeField
|
|
5
|
+
from django_filters.constants import EMPTY_VALUES
|
|
6
|
+
from django_filters.utils import get_model_field
|
|
4
7
|
|
|
5
8
|
from wbcore.filters.mixins import WBCoreFilterMixin
|
|
6
9
|
from wbcore.forms import DateRangeField, DateTimeRangeField
|
|
@@ -45,6 +48,13 @@ class DateRangeFilter(ShortcutAndPerformanceMixin, django_filters.Filter):
|
|
|
45
48
|
kwargs.setdefault("lookup_expr", "overlap")
|
|
46
49
|
super().__init__(*args, **kwargs)
|
|
47
50
|
|
|
51
|
+
@property
|
|
52
|
+
def is_range(self) -> bool:
|
|
53
|
+
if hasattr(self, "model"):
|
|
54
|
+
field = get_model_field(self.model, self.field_name)
|
|
55
|
+
return issubclass(field.__class__, RangeField)
|
|
56
|
+
return False
|
|
57
|
+
|
|
48
58
|
def _get_initial(self, *args):
|
|
49
59
|
initial = super()._get_initial(*args)
|
|
50
60
|
if initial is not None:
|
|
@@ -70,23 +80,38 @@ class DateRangeFilter(ShortcutAndPerformanceMixin, django_filters.Filter):
|
|
|
70
80
|
|
|
71
81
|
return representation, lookup_expr
|
|
72
82
|
|
|
73
|
-
|
|
74
|
-
|
|
83
|
+
def filter(self, qs, value):
|
|
84
|
+
if value in EMPTY_VALUES:
|
|
85
|
+
return qs
|
|
75
86
|
if value:
|
|
87
|
+
lower, upper = value.lower, value.upper
|
|
76
88
|
filters = {}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
89
|
+
is_field_range = self.is_range
|
|
90
|
+
if lower:
|
|
91
|
+
if is_field_range:
|
|
92
|
+
filters[f"{self.field_name}__startswith__gte"] = lower
|
|
93
|
+
else:
|
|
94
|
+
filters[f"{self.field_name}__gte"] = lower
|
|
95
|
+
|
|
96
|
+
if upper:
|
|
97
|
+
if is_field_range:
|
|
98
|
+
filters[f"{self.field_name}__endswith__lte"] = upper
|
|
99
|
+
else:
|
|
100
|
+
filters[f"{self.field_name}__lte"] = upper
|
|
101
|
+
|
|
102
|
+
if self.exclude:
|
|
103
|
+
qs = qs.exclude(**filters)
|
|
104
|
+
else:
|
|
105
|
+
qs = qs.filter(**filters)
|
|
106
|
+
return qs
|
|
83
107
|
|
|
84
108
|
|
|
85
109
|
class FinancialPerformanceDateRangeFilter(DateRangeFilter):
|
|
86
110
|
def __init__(self, *args, **kwargs):
|
|
87
|
-
super().__init__(performance_mode=True, shortcuts=financial_performance_shortcuts,
|
|
111
|
+
super().__init__(*args, performance_mode=True, shortcuts=financial_performance_shortcuts, **kwargs)
|
|
88
112
|
|
|
89
113
|
|
|
90
114
|
class DateTimeRangeFilter(DateRangeFilter):
|
|
91
115
|
field_class = DateTimeRangeField
|
|
92
116
|
initial_format = "%Y-%m-%dT%H:%M:%S%z"
|
|
117
|
+
filter_type = "datetimerange"
|
wbcore/filters/fields/models.py
CHANGED
|
@@ -33,8 +33,8 @@ class ModelChoiceFilterMixin(WBCoreFilterMixin):
|
|
|
33
33
|
"value": value_id,
|
|
34
34
|
"label": str(queryset.get(id=value_id)),
|
|
35
35
|
}
|
|
36
|
-
except ObjectDoesNotExist:
|
|
37
|
-
raise ParseError("Filter value invalid")
|
|
36
|
+
except ObjectDoesNotExist as e:
|
|
37
|
+
raise ParseError("Filter value invalid") from e
|
|
38
38
|
|
|
39
39
|
def get_representation(self, request, name, view):
|
|
40
40
|
representation, lookup_expr = super().get_representation(request, name, view)
|
wbcore/filters/filterset.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from collections import OrderedDict
|
|
2
3
|
from contextlib import suppress
|
|
4
|
+
from copy import copy
|
|
3
5
|
|
|
4
6
|
from django.contrib.postgres.fields import DateRangeField, DateTimeRangeField
|
|
5
7
|
from django.core.exceptions import FieldError
|
|
@@ -28,8 +30,8 @@ def _is_number(field):
|
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
class CustomFilterSetMetaClass(FilterSetMetaclass):
|
|
31
|
-
def __new__(cls,
|
|
32
|
-
new_class = super().__new__(cls,
|
|
33
|
+
def __new__(cls, *args, **kwargs):
|
|
34
|
+
new_class = super().__new__(cls, *args, **kwargs)
|
|
33
35
|
if _meta := getattr(new_class, "Meta", None):
|
|
34
36
|
for parent_field_name, child_fields in getattr(_meta, "flatten_fields", dict()).items():
|
|
35
37
|
if remote_field := getattr(_meta.model._meta.get_field(parent_field_name), "remote_field", None):
|
|
@@ -65,6 +67,7 @@ class CustomFilterSetMetaClass(FilterSetMetaclass):
|
|
|
65
67
|
|
|
66
68
|
|
|
67
69
|
class FilterSet(DjangoFilterSet, metaclass=CustomFilterSetMetaClass):
|
|
70
|
+
DEFAULT_EXCLUDE_FILTER_LOOKUP: str = "exclude"
|
|
68
71
|
FILTER_DEFAULTS = {
|
|
69
72
|
models.BooleanField: {"filter_class": fields.BooleanFilter},
|
|
70
73
|
models.NullBooleanField: {"filter_class": fields.BooleanFilter},
|
|
@@ -168,7 +171,7 @@ class FilterSet(DjangoFilterSet, metaclass=CustomFilterSetMetaClass):
|
|
|
168
171
|
for _, res in remote_filters:
|
|
169
172
|
if res:
|
|
170
173
|
for remote_filter_key, remote_filter in res.items():
|
|
171
|
-
|
|
174
|
+
remote_filter.column_field_name = remote_filter_key
|
|
172
175
|
self.filters[remote_filter_key] = remote_filter
|
|
173
176
|
|
|
174
177
|
@classmethod
|
|
@@ -196,12 +199,12 @@ class FilterSet(DjangoFilterSet, metaclass=CustomFilterSetMetaClass):
|
|
|
196
199
|
|
|
197
200
|
@classmethod
|
|
198
201
|
def get_filters(cls):
|
|
199
|
-
filters = super().get_filters()
|
|
202
|
+
filters = dict(super().get_filters())
|
|
200
203
|
remote_filters = add_filters.send(sender=cls.filter_class_for_remote_filter())
|
|
201
204
|
for _, res in remote_filters:
|
|
202
205
|
if res:
|
|
203
206
|
for remote_filter_key, remote_filter in res.items():
|
|
204
|
-
|
|
207
|
+
remote_filter.column_field_name = remote_filter_key
|
|
205
208
|
filters[remote_filter_key] = remote_filter
|
|
206
209
|
|
|
207
210
|
for field, help_text in getattr(cls, "help_texts", {}).items():
|
|
@@ -211,7 +214,20 @@ class FilterSet(DjangoFilterSet, metaclass=CustomFilterSetMetaClass):
|
|
|
211
214
|
for field, values in cls.get_dependency_map():
|
|
212
215
|
for value in values:
|
|
213
216
|
filters[field].depends_on.append({"field": value, "options": {}})
|
|
214
|
-
|
|
217
|
+
|
|
218
|
+
excluding_fields = {}
|
|
219
|
+
for name, field in filters.items():
|
|
220
|
+
# if allow_exclude is true, we add a copy of the field with the parameter exclude=True
|
|
221
|
+
# (to use `exclude` queryset method instead of `filter`) and add this with the suffix __{cls.DEFAULT_EXCLUDE_FILTER_LOOKUP}
|
|
222
|
+
if field.allow_exclude:
|
|
223
|
+
excluding_field = copy(field)
|
|
224
|
+
excluding_field.exclude = True
|
|
225
|
+
excluding_field.excluded_filter = True
|
|
226
|
+
excluding_field.hidden = True
|
|
227
|
+
excluding_field.required = False
|
|
228
|
+
excluding_fields[f"{name}__{cls.DEFAULT_EXCLUDE_FILTER_LOOKUP}"] = excluding_field
|
|
229
|
+
filters.update(excluding_fields)
|
|
230
|
+
return OrderedDict(filters)
|
|
215
231
|
|
|
216
232
|
def extract_required_field_labels(self):
|
|
217
233
|
return [label for label, filter in self.base_filters.items() if getattr(filter, "required", False)]
|
wbcore/filters/mixins.py
CHANGED
|
@@ -30,6 +30,10 @@ class WBCoreFilterMixin:
|
|
|
30
30
|
"label_format",
|
|
31
31
|
getattr(self, "default_label_format", "{{field_label}} {{operation_icon}} {{value_label}}"),
|
|
32
32
|
)
|
|
33
|
+
self.allow_exclude = kwargs.pop(
|
|
34
|
+
"allow_exclude", kwargs.get("method") is None
|
|
35
|
+
) # if False, we will not automatically add a similar filter "opposite" filter
|
|
36
|
+
self.excluded_filter = kwargs.pop("excluded_filter", False)
|
|
33
37
|
self.lookup_icon = kwargs.pop("lookup_icon", None)
|
|
34
38
|
self.lookup_label = kwargs.pop("lookup_label", None)
|
|
35
39
|
self.depends_on = kwargs.pop("depends_on", [])
|
|
@@ -90,6 +94,7 @@ class WBCoreFilterMixin:
|
|
|
90
94
|
"icon": get_lookup_icon(self.lookup_expr) if self.lookup_icon is None else self.lookup_icon,
|
|
91
95
|
"key": name,
|
|
92
96
|
"hidden": self.hidden,
|
|
97
|
+
"allow_exclude": self.allow_exclude,
|
|
93
98
|
"input_properties": {
|
|
94
99
|
"type": self.filter_type,
|
|
95
100
|
},
|
|
@@ -101,7 +106,6 @@ class WBCoreFilterMixin:
|
|
|
101
106
|
if initial is not None or self.allow_empty_initial:
|
|
102
107
|
lookup_expr["input_properties"]["initial"] = initial
|
|
103
108
|
|
|
104
|
-
|
|
105
|
-
lookup_expr["input_properties"]["required"] = True
|
|
109
|
+
lookup_expr["input_properties"]["required"] = self.required
|
|
106
110
|
representation["depends_on"] = self.depends_on
|
|
107
111
|
return representation, lookup_expr
|
wbcore/forms.py
CHANGED
|
@@ -3,7 +3,7 @@ from urllib.parse import unquote
|
|
|
3
3
|
|
|
4
4
|
from django import forms
|
|
5
5
|
from django.contrib.contenttypes.models import ContentType
|
|
6
|
-
from django.core.exceptions import ValidationError
|
|
6
|
+
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|
7
7
|
from django.forms import modelformset_factory
|
|
8
8
|
from django.forms.models import BaseModelFormSet
|
|
9
9
|
from psycopg.types.range import DateRange, TimestamptzRange
|
|
@@ -39,10 +39,10 @@ def nonrelated_inlineformset_factory(
|
|
|
39
39
|
"""
|
|
40
40
|
FormSet factory that sets an explicit queryset on new classes.
|
|
41
41
|
"""
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return
|
|
42
|
+
form = modelformset_factory(model, formset=formset, **kwargs)
|
|
43
|
+
form.real_queryset = queryset
|
|
44
|
+
form.save_new_instance = save_new_instance
|
|
45
|
+
return form
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
class ContentTypeMultiValueField(forms.fields.MultiValueField):
|
|
@@ -68,7 +68,7 @@ class ContentTypeMultiValueField(forms.fields.MultiValueField):
|
|
|
68
68
|
try:
|
|
69
69
|
content_type = ContentType.objects.get_for_id(content_type_id)
|
|
70
70
|
return content_type.get_object_for_this_type(id=object_id)
|
|
71
|
-
except
|
|
71
|
+
except ObjectDoesNotExist:
|
|
72
72
|
return None
|
|
73
73
|
|
|
74
74
|
|