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
wbcore/fsm/mixins.py
CHANGED
|
@@ -23,15 +23,15 @@ def get_method(transition, fsm_field_name):
|
|
|
23
23
|
class FSMViewSetMixinMetaclass(type):
|
|
24
24
|
"""Metaclass for dynamically creating all FSM Routes"""
|
|
25
25
|
|
|
26
|
-
def __new__(cls,
|
|
27
|
-
_class = super().__new__(cls,
|
|
26
|
+
def __new__(cls, *args, **kwargs):
|
|
27
|
+
_class = super().__new__(cls, *args, **kwargs)
|
|
28
28
|
|
|
29
29
|
# The class needs the field FSM_MODELFIELDS to know which transitions it needs to add
|
|
30
30
|
if hasattr(_class, "get_model"):
|
|
31
31
|
model = _class.get_model()
|
|
32
32
|
|
|
33
33
|
if model:
|
|
34
|
-
|
|
34
|
+
_class.FSM_BUTTONS = getattr(_class, "FSM_BUTTONS", set())
|
|
35
35
|
# The model potentially has multiple FSMFields, which needs to be iterated over
|
|
36
36
|
for field in filter(lambda f: isinstance(f, FSMField), model._meta.fields):
|
|
37
37
|
# Get all transitions, by calling the partialmethod defined by django-fsm
|
|
@@ -112,7 +112,10 @@ class FSMViewSetMixin(metaclass=FSMViewSetMixinMetaclass):
|
|
|
112
112
|
post_action_method(by=request.user)
|
|
113
113
|
# we extend the framework to allow action to successfully return but notify any possible warning. We use the message framework to communicate these warnings to the user
|
|
114
114
|
if warnings:
|
|
115
|
-
|
|
115
|
+
if isinstance(warnings, list):
|
|
116
|
+
html = "<ul>" + "".join(f"<li>{e}</li>" for e in warnings) + "</ul>"
|
|
117
|
+
else:
|
|
118
|
+
html = "<p>" + warnings + "</p>"
|
|
116
119
|
warning(request, html, extra_tags="auto_close=0")
|
|
117
120
|
|
|
118
121
|
serializer = serializer_class(instance=obj, context=serializer_context)
|
wbcore/markdown/models.py
CHANGED
|
@@ -17,7 +17,15 @@ class Asset(models.Model):
|
|
|
17
17
|
file = models.FileField(max_length=256, upload_to=upload_to)
|
|
18
18
|
content_type = models.CharField(max_length=32, null=True, blank=True)
|
|
19
19
|
file_url_name = models.CharField(max_length=1024, null=True, blank=True)
|
|
20
|
+
|
|
20
21
|
# public = models.BooleanField(default=True)
|
|
22
|
+
class Meta:
|
|
23
|
+
verbose_name = _("Asset")
|
|
24
|
+
verbose_name_plural = _("Assets")
|
|
25
|
+
db_table = "bridger_asset"
|
|
26
|
+
|
|
27
|
+
def __str__(self) -> str:
|
|
28
|
+
return str(self.id)
|
|
21
29
|
|
|
22
30
|
@property
|
|
23
31
|
def filename(self):
|
|
@@ -25,11 +33,6 @@ class Asset(models.Model):
|
|
|
25
33
|
return f"{self.id}{suffix}"
|
|
26
34
|
return self.id
|
|
27
35
|
|
|
28
|
-
class Meta:
|
|
29
|
-
verbose_name = _("Asset")
|
|
30
|
-
verbose_name_plural = _("Assets")
|
|
31
|
-
db_table = "bridger_asset"
|
|
32
|
-
|
|
33
36
|
|
|
34
37
|
@receiver(models.signals.pre_save, sender="wbcore.Asset")
|
|
35
38
|
def generate_content_type(sender, instance, **kwargs):
|
|
@@ -24,8 +24,8 @@ class ButtonConfig:
|
|
|
24
24
|
def __post_init__(self):
|
|
25
25
|
if post_init := getattr(super(), "__post_init__", None):
|
|
26
26
|
post_init()
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
if not self.label and not self.icon:
|
|
28
|
+
raise ValueError("No label or icon specified")
|
|
29
29
|
|
|
30
30
|
def __iter__(self):
|
|
31
31
|
if iter := getattr(super(), "__iter__", None):
|
|
@@ -57,8 +57,8 @@ class ButtonTypeMixin:
|
|
|
57
57
|
def __post_init__(self):
|
|
58
58
|
if post_init := getattr(super(), "__post_init__", None):
|
|
59
59
|
post_init()
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
if not hasattr(self, "button_type"):
|
|
61
|
+
raise TypeError("button_type cannot be None.")
|
|
62
62
|
|
|
63
63
|
def __iter__(self):
|
|
64
64
|
if iter := getattr(super(), "__iter__", None):
|
|
@@ -82,8 +82,8 @@ class ButtonUrlMixin:
|
|
|
82
82
|
def __post_init__(self):
|
|
83
83
|
if post_init := getattr(super(), "__post_init__", None):
|
|
84
84
|
post_init()
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
if bool(self.key) == bool(self.endpoint):
|
|
86
|
+
raise ValueError("Either key or endpoint has to be defined. (Not both)")
|
|
87
87
|
|
|
88
88
|
def __iter__(self):
|
|
89
89
|
if iter := getattr(super(), "__iter__", None):
|
|
@@ -23,7 +23,8 @@ class DropDownButton(ButtonTypeMixin, ButtonConfig):
|
|
|
23
23
|
if hasattr(super(), "__post_init__"):
|
|
24
24
|
super().__post_init__()
|
|
25
25
|
self.buttons = tuple(self.buttons)
|
|
26
|
-
|
|
26
|
+
if not isinstance(self.buttons, tuple):
|
|
27
|
+
raise TypeError(f"{type(self.buttons)} is not a tuple")
|
|
27
28
|
|
|
28
29
|
def serialize(self, request, **kwargs):
|
|
29
30
|
res = super().serialize(request, **kwargs)
|
|
@@ -29,7 +29,9 @@ class ButtonViewConfig(WBCoreViewConfig):
|
|
|
29
29
|
Returns:
|
|
30
30
|
Yield the serialized button, without duplicates and appends the module prefix to the remote button
|
|
31
31
|
"""
|
|
32
|
-
base_buttons = list(
|
|
32
|
+
base_buttons = list(
|
|
33
|
+
zip([None] * len(base_buttons), base_buttons, strict=False)
|
|
34
|
+
) # append an empty perfix for base buttons
|
|
33
35
|
for prefix, btn in parse_signal_received_for_module(remote_resources):
|
|
34
36
|
base_buttons.append((prefix, btn))
|
|
35
37
|
|
|
@@ -54,14 +56,14 @@ class ButtonViewConfig(WBCoreViewConfig):
|
|
|
54
56
|
FSM_WEIGHT = 100
|
|
55
57
|
|
|
56
58
|
def get_fsm_buttons(self) -> set:
|
|
57
|
-
if self.FSM_DROPDOWN and (
|
|
59
|
+
if self.FSM_DROPDOWN and (fsm_buttons := self.view.FSM_BUTTONS) and len(fsm_buttons) > 0:
|
|
58
60
|
return {
|
|
59
61
|
DropDownButton(
|
|
60
62
|
label=self.FSM_DROPDOWN_LABEL,
|
|
61
63
|
icon=self.FSM_DROPDOWN_ICON,
|
|
62
64
|
title=self.FSM_DROPDOWN_LABEL,
|
|
63
65
|
weight=self.FSM_WEIGHT,
|
|
64
|
-
buttons=tuple(
|
|
66
|
+
buttons=tuple(fsm_buttons),
|
|
65
67
|
)
|
|
66
68
|
}
|
|
67
69
|
return getattr(self.view, "FSM_BUTTONS", set())
|
|
@@ -41,8 +41,8 @@ class Operator(Enum):
|
|
|
41
41
|
operator_dict = {o.value: o for o in cls}
|
|
42
42
|
try:
|
|
43
43
|
return operator_dict[op]
|
|
44
|
-
except KeyError:
|
|
45
|
-
raise InvalidOperatorError(f"`{op}` is not a valid operator")
|
|
44
|
+
except KeyError as e:
|
|
45
|
+
raise InvalidOperatorError(f"`{op}` is not a valid operator") from e
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def fr(fractions: int) -> str:
|
|
@@ -9,8 +9,8 @@ class Condition:
|
|
|
9
9
|
value: str | float | int | bool
|
|
10
10
|
|
|
11
11
|
def __post_init__(self) -> None:
|
|
12
|
-
if self.operator == Operator.EXISTS:
|
|
13
|
-
|
|
12
|
+
if self.operator == Operator.EXISTS and not isinstance(self.value, bool):
|
|
13
|
+
raise TypeError(f"{Operator.EXISTS.value} is only compatible with bool")
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
@dataclass(unsafe_hash=True)
|
|
@@ -19,7 +19,8 @@ class FormattingRule:
|
|
|
19
19
|
condition: Condition | tuple | list[tuple] | None = None
|
|
20
20
|
|
|
21
21
|
def __post_init__(self) -> None:
|
|
22
|
-
|
|
22
|
+
if not self.style:
|
|
23
|
+
raise ValueError("Style cannot be empty")
|
|
23
24
|
|
|
24
25
|
def __iter__(self):
|
|
25
26
|
yield "style", self.style
|
|
@@ -38,10 +39,8 @@ class Formatting:
|
|
|
38
39
|
column: str | None = None
|
|
39
40
|
|
|
40
41
|
def __post_init__(self) -> None:
|
|
41
|
-
if self.column is None:
|
|
42
|
-
|
|
43
|
-
[not bool(rule.condition) for rule in self.formatting_rules]
|
|
44
|
-
), "Specifying conditions, without a reference column is not possible."
|
|
42
|
+
if self.column is None and not all([not bool(rule.condition) for rule in self.formatting_rules]):
|
|
43
|
+
raise ValueError("Specifying conditions, without a reference column is not possible.")
|
|
45
44
|
|
|
46
45
|
def __iter__(self):
|
|
47
46
|
yield "column", self.column
|
|
@@ -59,8 +59,9 @@ class Field:
|
|
|
59
59
|
size_to_fit: bool = True
|
|
60
60
|
|
|
61
61
|
def __post_init__(self):
|
|
62
|
-
|
|
63
|
-
self.key
|
|
62
|
+
self.identifier = (
|
|
63
|
+
self.key if self.key else slugify(str(self.label))
|
|
64
|
+
) # we cast to str explicitly in case label is in a translation wrapper
|
|
64
65
|
|
|
65
66
|
def iterate_leaf_fields(self, aggregated_parent_label: str = ""):
|
|
66
67
|
label = self.label
|
|
@@ -73,7 +74,7 @@ class Field:
|
|
|
73
74
|
yield self.key, label
|
|
74
75
|
|
|
75
76
|
def serialize(self, parent_identifier: str | None = None):
|
|
76
|
-
identifier = parent_identifier + "_" + self.
|
|
77
|
+
identifier = parent_identifier + "_" + self.identifier if parent_identifier else self.identifier
|
|
77
78
|
repr = {
|
|
78
79
|
"identifier": identifier,
|
|
79
80
|
"key": self.key,
|
|
@@ -144,10 +145,8 @@ class Legend:
|
|
|
144
145
|
key: str | None = None
|
|
145
146
|
|
|
146
147
|
def __post_init__(self):
|
|
147
|
-
if self.key:
|
|
148
|
-
|
|
149
|
-
[item.value is not None for item in self.items]
|
|
150
|
-
), "If key is set, all items need to specify a value."
|
|
148
|
+
if self.key and not all([item.value is not None for item in self.items]):
|
|
149
|
+
raise ValueError("If key is set, all items need to specify a value.")
|
|
151
150
|
|
|
152
151
|
def __iter__(self):
|
|
153
152
|
if self.label:
|
|
@@ -10,6 +10,9 @@ class Preset(models.Model):
|
|
|
10
10
|
display_identifier = models.CharField(max_length=512)
|
|
11
11
|
display = models.JSONField(null=True, blank=True)
|
|
12
12
|
|
|
13
|
+
def __str__(self) -> str:
|
|
14
|
+
return f"{self.title} - {self.user} ({self.display_identifier})"
|
|
15
|
+
|
|
13
16
|
|
|
14
17
|
class AppliedPreset(models.Model):
|
|
15
18
|
user = models.ForeignKey(to=get_user_model(), related_name="applied_presets", on_delete=models.CASCADE)
|
|
@@ -18,3 +21,6 @@ class AppliedPreset(models.Model):
|
|
|
18
21
|
to=Preset, related_name="applied_presets", on_delete=models.SET_NULL, null=True, blank=True
|
|
19
22
|
)
|
|
20
23
|
display = models.JSONField(null=True, blank=True)
|
|
24
|
+
|
|
25
|
+
def __str__(self) -> str:
|
|
26
|
+
return f"{self.display_identifier_path} ({self.user})"
|
|
@@ -10,8 +10,13 @@ class FieldsViewConfig(WBCoreViewConfig):
|
|
|
10
10
|
def get_metadata(self) -> dict:
|
|
11
11
|
fields = defaultdict(dict)
|
|
12
12
|
if (serializer_class := getattr(self.view, "get_serializer", None)) and (serializer := serializer_class()):
|
|
13
|
+
related_key_fields = []
|
|
13
14
|
for field_name, field in serializer.fields.items():
|
|
14
15
|
field_key, field_representation = field.get_representation(self.request, field_name)
|
|
16
|
+
# we need to get the representation of the related field last so that the key update properly (priority to the related field values)
|
|
17
|
+
if "related_key" in field_representation:
|
|
18
|
+
related_key_fields.append((field_key, field_representation))
|
|
19
|
+
fields[field_key].update(field_representation)
|
|
20
|
+
for field_key, field_representation in related_key_fields:
|
|
15
21
|
fields[field_key].update(field_representation)
|
|
16
|
-
|
|
17
22
|
return fields
|
|
@@ -25,16 +25,17 @@ class FilterFieldsViewConfig(WBCoreViewConfig):
|
|
|
25
25
|
hidden_fields.extend(getattr(filterset_class_meta, "hidden_fields", []))
|
|
26
26
|
filters.update(getattr(filterset_class_meta, "df_fields", {}))
|
|
27
27
|
for name, field in filters.items():
|
|
28
|
-
field.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
if not field.excluded_filter:
|
|
29
|
+
field.parent = filterset
|
|
30
|
+
if res := field.get_representation(self.request, name, self.view):
|
|
31
|
+
representation, lookup_expr = res
|
|
32
|
+
if name in hidden_fields:
|
|
33
|
+
lookup_expr["hidden"] = True
|
|
34
|
+
if field.key in filter_fields:
|
|
35
|
+
filter_fields[field.key]["lookup_expr"].append(lookup_expr)
|
|
36
|
+
else:
|
|
37
|
+
filter_fields[field.key] = representation
|
|
38
|
+
filter_fields[field.key]["lookup_expr"] = [lookup_expr]
|
|
39
|
+
filter_fields[field.key]["label"] = field.label
|
|
39
40
|
|
|
40
41
|
return filter_fields
|
wbcore/models/fields.py
CHANGED
|
@@ -5,10 +5,10 @@ from django.db.models import DecimalField, Field, FloatField, PositiveIntegerFie
|
|
|
5
5
|
class AbstractDynamicField(Field):
|
|
6
6
|
dependencies = []
|
|
7
7
|
|
|
8
|
-
def __init__(self, *args, dependencies=
|
|
8
|
+
def __init__(self, *args, dependencies: list | None = None, **kwargs):
|
|
9
9
|
blank = kwargs.pop("blank", True)
|
|
10
10
|
null = kwargs.pop("null", True)
|
|
11
|
-
self.dependencies = dependencies
|
|
11
|
+
self.dependencies = dependencies if dependencies else []
|
|
12
12
|
super().__init__(*args, blank=blank, null=null, **kwargs)
|
|
13
13
|
|
|
14
14
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from rest_framework import permissions
|
|
2
|
-
from rest_framework.permissions import
|
|
2
|
+
from rest_framework.permissions import IsAuthenticated
|
|
3
3
|
|
|
4
4
|
from wbcore.enums import WidgetType
|
|
5
5
|
|
|
@@ -40,7 +40,7 @@ class RestAPIModelPermissions(permissions.DjangoModelPermissions):
|
|
|
40
40
|
return request.user.has_perms(perms)
|
|
41
41
|
|
|
42
42
|
|
|
43
|
-
class IsInternalUser(
|
|
43
|
+
class IsInternalUser(IsAuthenticated):
|
|
44
44
|
def has_permission(self, request, view) -> bool:
|
|
45
45
|
return is_internal_user(request.user, True)
|
|
46
46
|
|
wbcore/permissions/utils.py
CHANGED
|
@@ -17,10 +17,10 @@ def perm_to_permission(perm: str) -> Permission:
|
|
|
17
17
|
"""
|
|
18
18
|
try:
|
|
19
19
|
app_label, codename = perm.split(".", 1)
|
|
20
|
-
except IndexError:
|
|
20
|
+
except IndexError as e:
|
|
21
21
|
raise AttributeError(
|
|
22
22
|
"The format of identifier string permission (perm) is wrong. " "It should be in 'app_label.codename'."
|
|
23
|
-
)
|
|
23
|
+
) from e
|
|
24
24
|
else:
|
|
25
25
|
permission = Permission.objects.get(content_type__app_label=app_label, codename=codename)
|
|
26
26
|
return permission
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
from contextlib import suppress
|
|
2
|
+
|
|
1
3
|
from django.contrib.contenttypes.models import ContentType
|
|
4
|
+
from django.core.exceptions import ObjectDoesNotExist
|
|
2
5
|
from django.utils.translation import gettext as _
|
|
3
6
|
|
|
4
7
|
from wbcore.metadata.configs.titles import TitleViewConfig
|
|
@@ -9,10 +12,8 @@ class VersionTitleConfig(TitleViewConfig):
|
|
|
9
12
|
if (content_type_id := self.view.request.GET.get("content_type", None)) and (
|
|
10
13
|
object_id := self.view.request.GET.get("object_id", None)
|
|
11
14
|
):
|
|
12
|
-
|
|
15
|
+
with suppress(ObjectDoesNotExist):
|
|
13
16
|
content_type = ContentType.objects.get(id=content_type_id)
|
|
14
17
|
obj = content_type.model_class().objects.get(id=object_id)
|
|
15
18
|
return _("Versions For {obj}").format(obj=str(obj))
|
|
16
|
-
except Exception:
|
|
17
|
-
pass
|
|
18
19
|
return _("Versions")
|
wbcore/serializers/__init__.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
from datetime import timedelta
|
|
1
|
+
from datetime import date, datetime, timedelta
|
|
2
2
|
|
|
3
3
|
import pytz
|
|
4
|
-
from psycopg.types.range import DateRange, TimestamptzRange
|
|
4
|
+
from psycopg.types.range import DateRange, TimestampRange, TimestamptzRange
|
|
5
5
|
from rest_framework import serializers
|
|
6
|
+
from rest_framework.settings import api_settings
|
|
6
7
|
from timezone_field.choices import standard, with_gmt_offset
|
|
7
8
|
from timezone_field.rest_framework import TimeZoneSerializerField
|
|
8
9
|
|
|
@@ -108,11 +109,39 @@ class DateTimeRangeField(RangeMixin, ShortcutMixin, serializers.DateTimeField):
|
|
|
108
109
|
representation["upper_time_choices"] = self.upper_time_choices(self, request)
|
|
109
110
|
else:
|
|
110
111
|
representation["upper_time_choices"] = self.upper_time_choices
|
|
111
|
-
if timezone := getattr(self, "timezone", None):
|
|
112
|
-
representation["timezone"] = str(timezone)
|
|
113
112
|
return key, representation
|
|
114
113
|
|
|
115
114
|
|
|
115
|
+
class TimeRange(RangeMixin, ShortcutMixin, serializers.TimeField):
|
|
116
|
+
field_type = WBCoreType.TIMERANGE.value
|
|
117
|
+
internal_field = TimestampRange
|
|
118
|
+
|
|
119
|
+
def __init__(self, *args, timerange_fields: tuple[str, str] | None = None, **kwargs):
|
|
120
|
+
self.timerange_fields = timerange_fields
|
|
121
|
+
super().__init__(*args, **kwargs)
|
|
122
|
+
self.default_date_repr = date.min.strftime(getattr(self, "format", api_settings.DATE_FORMAT))
|
|
123
|
+
if self.timerange_fields:
|
|
124
|
+
self.source = "*"
|
|
125
|
+
|
|
126
|
+
def _transform_range(self, lower, upper, **kwargs):
|
|
127
|
+
if isinstance(lower, datetime):
|
|
128
|
+
lower = lower.time()
|
|
129
|
+
if isinstance(upper, datetime):
|
|
130
|
+
upper = upper.time()
|
|
131
|
+
return lower, upper
|
|
132
|
+
|
|
133
|
+
def get_attribute(self, instance):
|
|
134
|
+
if self.timerange_fields:
|
|
135
|
+
return [getattr(instance, self.timerange_fields[0]), getattr(instance, self.timerange_fields[1])]
|
|
136
|
+
return super().get_attribute(instance)
|
|
137
|
+
|
|
138
|
+
def to_internal_value(self, data):
|
|
139
|
+
ts_range = super().to_internal_value(data)
|
|
140
|
+
if self.timerange_fields:
|
|
141
|
+
return dict(zip(self.timerange_fields, (ts_range.lower, ts_range.upper), strict=False))
|
|
142
|
+
return ts_range
|
|
143
|
+
|
|
144
|
+
|
|
116
145
|
class DurationField(NumberFieldMixin, WBCoreSerializerFieldMixin, serializers.DurationField):
|
|
117
146
|
field_type = WBCoreType.DURATION.value
|
|
118
147
|
|
|
@@ -126,7 +155,7 @@ class TimeZoneField(WBCoreSerializerFieldMixin, TimeZoneSerializerField):
|
|
|
126
155
|
|
|
127
156
|
def __init__(self, choices=None, choices_display=None, *args, **kwargs):
|
|
128
157
|
if choices:
|
|
129
|
-
values, displays = zip(*choices)
|
|
158
|
+
values, displays = zip(*choices, strict=False)
|
|
130
159
|
else:
|
|
131
160
|
values = pytz.common_timezones
|
|
132
161
|
displays = None
|
|
@@ -136,7 +165,7 @@ class TimeZoneField(WBCoreSerializerFieldMixin, TimeZoneSerializerField):
|
|
|
136
165
|
elif choices_display == "STANDARD":
|
|
137
166
|
choices = standard(values)
|
|
138
167
|
elif choices_display is None:
|
|
139
|
-
choices = zip(values, displays) if displays else standard(values)
|
|
168
|
+
choices = zip(values, displays, strict=False) if displays else standard(values)
|
|
140
169
|
else:
|
|
141
170
|
raise ValueError(f"Unrecognized value for kwarg 'choices_display' of '{choices_display}'")
|
|
142
171
|
|
|
@@ -91,7 +91,7 @@ class DynamicButtonField(WBCoreSerializerFieldMixin, serializers.ReadOnlyField):
|
|
|
91
91
|
)
|
|
92
92
|
for prefix, btns in dynamic_buttons:
|
|
93
93
|
for btn in btns:
|
|
94
|
-
|
|
94
|
+
btn.prefix_key = prefix
|
|
95
95
|
buttons.append(btn.serialize(request))
|
|
96
96
|
if (view := self.parent.context.get("view", None)) and not (getattr(view, "action", "list") == "list"):
|
|
97
97
|
for _, button_func in getmembers(self.parent.__class__, _is_instance_dynamic_button):
|
wbcore/serializers/fields/fsm.py
CHANGED
|
@@ -8,7 +8,7 @@ class FSMStatusField(CharField):
|
|
|
8
8
|
def __init__(self, *args, **kwargs):
|
|
9
9
|
self.choices = kwargs.pop("choices")
|
|
10
10
|
read_only = kwargs.pop("read_only", True)
|
|
11
|
-
super().__init__(read_only=read_only,
|
|
11
|
+
super().__init__(*args, read_only=read_only, **kwargs)
|
|
12
12
|
|
|
13
13
|
def get_representation(self, request, field_name) -> tuple[str, dict]:
|
|
14
14
|
key, representation = super().get_representation(request, field_name)
|
|
@@ -100,5 +100,5 @@ class SparklineField(WBCoreSerializerFieldMixin, serializers.ListField):
|
|
|
100
100
|
def to_representation(self, obj):
|
|
101
101
|
representation = [[]] # if row is [] or null, we default to an empty list of list
|
|
102
102
|
if (x_data := getattr(obj, self.x_data_label, None)) and (y_data := getattr(obj, self.y_data_label, None)):
|
|
103
|
-
representation = zip(x_data, y_data)
|
|
103
|
+
representation = zip(x_data, y_data, strict=False)
|
|
104
104
|
return representation
|
|
@@ -10,8 +10,10 @@ logger = logging.getLogger(__name__)
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def decorator(position: str, value: str, decorator_type: str = "icon") -> dict:
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
if position not in ("left", "right"):
|
|
14
|
+
raise ValueError("Decorator Position can only be right or left")
|
|
15
|
+
if decorator_type not in ("icon", "text"):
|
|
16
|
+
raise ValueError("Decorator Type can only be icon or text")
|
|
15
17
|
return {"position": position, "value": value, "type": decorator_type}
|
|
16
18
|
|
|
17
19
|
|
|
@@ -31,6 +33,8 @@ class WBCoreSerializerFieldMixin:
|
|
|
31
33
|
read_only=False,
|
|
32
34
|
copyable=None,
|
|
33
35
|
related_key=None,
|
|
36
|
+
on_unsatisfied_deps="read_only",
|
|
37
|
+
clear_dependent_fields=True,
|
|
34
38
|
**kwargs,
|
|
35
39
|
):
|
|
36
40
|
if not decorators:
|
|
@@ -48,6 +52,8 @@ class WBCoreSerializerFieldMixin:
|
|
|
48
52
|
self._callable_read_only = read_only
|
|
49
53
|
read_only = True
|
|
50
54
|
self.related_key = related_key
|
|
55
|
+
self.on_unsatisfied_deps = on_unsatisfied_deps
|
|
56
|
+
self.clear_dependent_fields = clear_dependent_fields
|
|
51
57
|
super().__init__(*args, read_only=read_only, **kwargs)
|
|
52
58
|
|
|
53
59
|
def _evaluate_read_only(self, field_name, parent):
|
|
@@ -105,10 +111,8 @@ class WBCoreSerializerFieldMixin:
|
|
|
105
111
|
|
|
106
112
|
default = getattr(self, "default", None)
|
|
107
113
|
if default is None or default == empty or default == NOT_PROVIDED:
|
|
108
|
-
|
|
114
|
+
with suppress(Exception): # TODO Add some explicit exception handling
|
|
109
115
|
default = self.parent.Meta.model._meta._forward_fields_map[field_name].default
|
|
110
|
-
except Exception: # TODO Add some explicit exception handling
|
|
111
|
-
pass
|
|
112
116
|
|
|
113
117
|
if default is not None and default != empty and default != NOT_PROVIDED:
|
|
114
118
|
if callable(default):
|
|
@@ -131,6 +135,10 @@ class WBCoreSerializerFieldMixin:
|
|
|
131
135
|
representation["math"] = self.math
|
|
132
136
|
if self.copyable:
|
|
133
137
|
representation["copyable"] = self.copyable
|
|
138
|
+
if self.on_unsatisfied_deps != "read_only":
|
|
139
|
+
representation["on_unsatisfied_deps"] = self.on_unsatisfied_deps
|
|
140
|
+
if self.clear_dependent_fields is not True:
|
|
141
|
+
representation["clear_dependent_fields"] = self.clear_dependent_fields
|
|
134
142
|
return field_name, representation
|
|
135
143
|
|
|
136
144
|
def validate_empty_values(self, data):
|
|
@@ -29,7 +29,7 @@ class WBCoreManyRelatedField(ListFieldMixin, WBCoreSerializerFieldMixin, ManyRel
|
|
|
29
29
|
self.child_relation.context["view"] = self.view
|
|
30
30
|
self.child_relation._evaluate_read_only(field_name, parent)
|
|
31
31
|
if not self.child_relation.read_only and hasattr(self.child_relation, "_queryset"):
|
|
32
|
-
|
|
32
|
+
self.child_relation.queryset = self.child_relation._queryset
|
|
33
33
|
|
|
34
34
|
def get_representation(self, request: Request, field_name: str) -> tuple[str, dict]:
|
|
35
35
|
key, representation = self.child_relation.get_representation(request, field_name)
|
|
@@ -58,7 +58,7 @@ class PrimaryKeyRelatedField(WBCoreSerializerFieldMixin, serializers.PrimaryKeyR
|
|
|
58
58
|
)
|
|
59
59
|
|
|
60
60
|
def __init__(self, *args, queryset=None, read_only=False, **kwargs):
|
|
61
|
-
self.field_type = kwargs.pop("field_type", WBCoreType.
|
|
61
|
+
self.field_type = kwargs.pop("field_type", WBCoreType.PRIMARY_KEY.value)
|
|
62
62
|
if callable(read_only) and queryset is not None:
|
|
63
63
|
self._queryset = queryset # we unset any given queryset to be compliant with the RelatedField assertion
|
|
64
64
|
queryset = None
|
|
@@ -72,7 +72,7 @@ class PrimaryKeyRelatedField(WBCoreSerializerFieldMixin, serializers.PrimaryKeyR
|
|
|
72
72
|
super().bind(field_name, parent)
|
|
73
73
|
# In case we had to unset the queryset attribute because read_only was a callable, we reinstate it here.
|
|
74
74
|
if not self.read_only and hasattr(self, "_queryset"):
|
|
75
|
-
|
|
75
|
+
self.queryset = self._queryset
|
|
76
76
|
|
|
77
77
|
@classmethod
|
|
78
78
|
def many_init(cls, *args, **kwargs):
|
|
@@ -103,7 +103,7 @@ class PrimaryKeyRelatedField(WBCoreSerializerFieldMixin, serializers.PrimaryKeyR
|
|
|
103
103
|
# In case we annotate the representation, we need to ensure that the value is an object
|
|
104
104
|
if isinstance(value, (list, tuple, set)):
|
|
105
105
|
return [self.to_representation(d) for d in value]
|
|
106
|
-
|
|
106
|
+
with suppress(Exception): # TODO: investigate what exception are we expecting here
|
|
107
107
|
if isinstance(value, str):
|
|
108
108
|
try:
|
|
109
109
|
value = int(value)
|
|
@@ -112,8 +112,6 @@ class PrimaryKeyRelatedField(WBCoreSerializerFieldMixin, serializers.PrimaryKeyR
|
|
|
112
112
|
if isinstance(value, int):
|
|
113
113
|
value = PKOnlyObject(value)
|
|
114
114
|
return super().to_representation(value)
|
|
115
|
-
except Exception:
|
|
116
|
-
pass
|
|
117
115
|
return None
|
|
118
116
|
|
|
119
117
|
def get_queryset(self):
|
|
@@ -54,7 +54,7 @@ class CodeField(CharField):
|
|
|
54
54
|
try:
|
|
55
55
|
compile(data, "", "exec")
|
|
56
56
|
except Exception as e:
|
|
57
|
-
raise ValidationError(_("Compiling script failed with the exception: {}".format(e)))
|
|
57
|
+
raise ValidationError(_("Compiling script failed with the exception: {}".format(e))) from e
|
|
58
58
|
return super().to_internal_value(data)
|
|
59
59
|
|
|
60
60
|
|
|
@@ -49,8 +49,8 @@ def validate_nested_representation(instance, value):
|
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
class WBCoreSerializerMetaClass(SerializerMetaclass):
|
|
52
|
-
def __new__(cls,
|
|
53
|
-
_class = super().__new__(cls,
|
|
52
|
+
def __new__(cls, *args, **kwargs): # noqa: C901
|
|
53
|
+
_class = super().__new__(cls, *args, **kwargs)
|
|
54
54
|
|
|
55
55
|
if _meta := getattr(_class, "Meta", None):
|
|
56
56
|
model = _meta.model
|
|
@@ -334,6 +334,7 @@ class RepresentationSerializer(WBCoreSerializerFieldMixin, ModelSerializer):
|
|
|
334
334
|
getattr(self, "optional_get_parameters", None),
|
|
335
335
|
)
|
|
336
336
|
self.tree_config = tree_config
|
|
337
|
+
self.select_first_choice = kwargs.pop("select_first_choice", getattr(self, "select_first_choice", None))
|
|
337
338
|
super().__init__(*args, **kwargs)
|
|
338
339
|
|
|
339
340
|
def to_representation(self, value):
|
|
@@ -410,6 +411,9 @@ class RepresentationSerializer(WBCoreSerializerFieldMixin, ModelSerializer):
|
|
|
410
411
|
},
|
|
411
412
|
}
|
|
412
413
|
|
|
414
|
+
if self.select_first_choice:
|
|
415
|
+
representation["select_first_choice"] = True
|
|
416
|
+
|
|
413
417
|
if self.help_text:
|
|
414
418
|
representation["help_text"] = self.help_text
|
|
415
419
|
|
wbcore/tasks.py
CHANGED
|
@@ -45,7 +45,7 @@ def recompute_computed_str(debug: bool = False):
|
|
|
45
45
|
When this task is executed, it will loop over all objects that inherit from ComplexToStringMixin and compare their current computed_str value with the expected one.
|
|
46
46
|
If different, the expected one is saved in place.
|
|
47
47
|
"""
|
|
48
|
-
|
|
48
|
+
bulk_size = 1000
|
|
49
49
|
for subclass in get_inheriting_subclasses(ComplexToStringMixin):
|
|
50
50
|
if getattr(subclass, "COMPUTED_STR_RECOMPUTE_PERIODICALLY", True):
|
|
51
51
|
objs = []
|
|
@@ -59,7 +59,7 @@ def recompute_computed_str(debug: bool = False):
|
|
|
59
59
|
if new_computed_str != instance.computed_str:
|
|
60
60
|
instance.computed_str = new_computed_str
|
|
61
61
|
objs.append(instance)
|
|
62
|
-
if len(objs) %
|
|
62
|
+
if len(objs) % bulk_size == 0:
|
|
63
63
|
subclass.objects.bulk_update(objs, ["computed_str"])
|
|
64
64
|
objs = []
|
|
65
65
|
if objs:
|
|
@@ -208,7 +208,7 @@
|
|
|
208
208
|
width: 100%;
|
|
209
209
|
}
|
|
210
210
|
.fixed {
|
|
211
|
-
width:
|
|
211
|
+
width: 750px;
|
|
212
212
|
}
|
|
213
213
|
#body-table {
|
|
214
214
|
width: 100%;
|
|
@@ -248,8 +248,8 @@
|
|
|
248
248
|
}
|
|
249
249
|
#body-table .content-row > .body-content {
|
|
250
250
|
background-color: #fff;
|
|
251
|
-
padding-right:
|
|
252
|
-
padding-left:
|
|
251
|
+
padding-right: 15px;
|
|
252
|
+
padding-left: 15px;
|
|
253
253
|
}
|
|
254
254
|
#body-table #spacer-row > .spacer-left,
|
|
255
255
|
#body-table #spacer-row > .spacer-middle,
|