wbcore 1.54.10__py2.py3-none-any.whl → 1.58.2__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. wbcore/cache/decorators.py +3 -3
  2. wbcore/cache/registry.py +3 -2
  3. wbcore/configs/decorators.py +1 -1
  4. wbcore/configurations/configurations/apps.py +2 -2
  5. wbcore/configurations/configurations/authentication.py +1 -1
  6. wbcore/configurations/configurations/base.py +1 -1
  7. wbcore/configurations/configurations/cache.py +1 -1
  8. wbcore/configurations/configurations/maintenance.py +1 -1
  9. wbcore/configurations/configurations/media.py +1 -1
  10. wbcore/configurations/configurations/middleware.py +1 -1
  11. wbcore/configurations/configurations/rest_framework.py +1 -1
  12. wbcore/configurations/configurations/static.py +1 -1
  13. wbcore/configurations/configurations/wbcore.py +1 -1
  14. wbcore/content_type/serializers.py +1 -1
  15. wbcore/content_type/utils.py +3 -3
  16. wbcore/contrib/agenda/viewsets/calendar_items.py +7 -7
  17. wbcore/contrib/ai/llm/config.py +1 -1
  18. wbcore/contrib/authentication/admin.py +2 -2
  19. wbcore/contrib/authentication/filters.py +0 -1
  20. wbcore/contrib/authentication/models/users.py +3 -3
  21. wbcore/contrib/authentication/models/users_activities.py +1 -1
  22. wbcore/contrib/authentication/serializers/users.py +2 -2
  23. wbcore/contrib/authentication/tests/test_tokens.py +3 -3
  24. wbcore/contrib/authentication/tests/test_users.py +0 -1
  25. wbcore/contrib/authentication/viewsets/user_activities.py +2 -1
  26. wbcore/contrib/authentication/viewsets/users.py +6 -4
  27. wbcore/contrib/color/models.py +2 -1
  28. wbcore/contrib/currency/factories.py +1 -1
  29. wbcore/contrib/currency/import_export/backends/fixerio/currency_fx_rates.py +3 -1
  30. wbcore/contrib/currency/models.py +28 -8
  31. wbcore/contrib/currency/serializers.py +5 -1
  32. wbcore/contrib/currency/tests/test_serializers.py +7 -3
  33. wbcore/contrib/currency/tests/test_viewsets.py +1 -1
  34. wbcore/contrib/currency/viewsets/currency.py +2 -2
  35. wbcore/contrib/dataloader/utils.py +2 -2
  36. wbcore/contrib/directory/factories/__init__.py +1 -1
  37. wbcore/contrib/directory/factories/entries.py +1 -1
  38. wbcore/contrib/directory/models/contacts.py +2 -2
  39. wbcore/contrib/directory/models/entries.py +18 -4
  40. wbcore/contrib/directory/models/relationships.py +25 -30
  41. wbcore/contrib/directory/permissions.py +6 -0
  42. wbcore/contrib/directory/serializers/companies.py +15 -8
  43. wbcore/contrib/directory/serializers/contacts.py +8 -8
  44. wbcore/contrib/directory/serializers/entries.py +24 -15
  45. wbcore/contrib/directory/serializers/entry_representations.py +4 -2
  46. wbcore/contrib/directory/serializers/persons.py +8 -9
  47. wbcore/contrib/directory/serializers/relationships.py +2 -2
  48. wbcore/contrib/directory/tests/conftest.py +2 -0
  49. wbcore/contrib/directory/tests/disable_signals.py +11 -1
  50. wbcore/contrib/directory/tests/signals.py +2 -2
  51. wbcore/contrib/directory/tests/test_models.py +88 -66
  52. wbcore/contrib/directory/tests/test_serializers.py +1 -1
  53. wbcore/contrib/directory/tests/test_viewsets.py +8 -8
  54. wbcore/contrib/directory/viewsets/buttons/__init__.py +1 -1
  55. wbcore/contrib/directory/viewsets/buttons/relationships.py +32 -0
  56. wbcore/contrib/directory/viewsets/contacts.py +6 -6
  57. wbcore/contrib/directory/viewsets/display/__init__.py +1 -1
  58. wbcore/contrib/directory/viewsets/display/entries.py +51 -36
  59. wbcore/contrib/directory/viewsets/display/relationships.py +22 -22
  60. wbcore/contrib/directory/viewsets/entries.py +4 -5
  61. wbcore/contrib/directory/viewsets/previews/entries.py +3 -3
  62. wbcore/contrib/directory/viewsets/relationships.py +16 -2
  63. wbcore/contrib/directory/viewsets/titles/relationships.py +2 -3
  64. wbcore/contrib/documents/filters.py +0 -2
  65. wbcore/contrib/example_app/models.py +4 -4
  66. wbcore/contrib/example_app/serializers/person_team.py +4 -4
  67. wbcore/contrib/example_app/tests/e2e/test_teams.py +1 -1
  68. wbcore/contrib/geography/tests/test_viewsets.py +1 -1
  69. wbcore/contrib/guardian/tests/test_model_mixins.py +3 -3
  70. wbcore/contrib/guardian/tests/test_tasks.py +9 -9
  71. wbcore/contrib/guardian/tests/test_viewsets.py +2 -2
  72. wbcore/contrib/icons/backends/default.py +1 -0
  73. wbcore/contrib/icons/backends/material.py +1 -0
  74. wbcore/contrib/icons/icons.py +5 -8
  75. wbcore/contrib/io/exceptions.py +8 -0
  76. wbcore/contrib/io/import_export/backends/stream.py +2 -2
  77. wbcore/contrib/io/imports.py +10 -5
  78. wbcore/contrib/io/models.py +17 -14
  79. wbcore/contrib/io/serializers.py +2 -2
  80. wbcore/contrib/io/tests/test_backends.py +1 -1
  81. wbcore/contrib/io/tests/test_imports.py +1 -1
  82. wbcore/contrib/io/viewset_mixins.py +4 -4
  83. wbcore/contrib/notifications/dispatch.py +18 -7
  84. wbcore/contrib/pandas/filterset.py +8 -7
  85. wbcore/contrib/pandas/views.py +7 -5
  86. wbcore/contrib/tags/models/tags.py +4 -1
  87. wbcore/contrib/workflow/factories/display.py +2 -2
  88. wbcore/contrib/workflow/models/data.py +7 -4
  89. wbcore/contrib/workflow/models/process.py +2 -2
  90. wbcore/contrib/workflow/serializers/data.py +8 -8
  91. wbcore/contrib/workflow/tests/test_models/test_condition.py +1 -1
  92. wbcore/contrib/workflow/workflows/assignees.py +4 -4
  93. wbcore/dynamic_preferences_registry.py +23 -9
  94. wbcore/enums.py +2 -1
  95. wbcore/filters/fields/content_type.py +5 -4
  96. wbcore/filters/fields/datetime.py +34 -9
  97. wbcore/filters/fields/models.py +2 -2
  98. wbcore/filters/filterset.py +22 -6
  99. wbcore/filters/mixins.py +6 -2
  100. wbcore/forms.py +6 -6
  101. wbcore/fsm/markdown_extensions.py +1 -1
  102. wbcore/fsm/mixins.py +7 -4
  103. wbcore/markdown/models.py +8 -5
  104. wbcore/metadata/configs/buttons/bases.py +6 -6
  105. wbcore/metadata/configs/buttons/buttons.py +2 -1
  106. wbcore/metadata/configs/buttons/view_config.py +5 -3
  107. wbcore/metadata/configs/display/display.py +2 -2
  108. wbcore/metadata/configs/display/formatting.py +6 -7
  109. wbcore/metadata/configs/display/list_display.py +6 -7
  110. wbcore/metadata/configs/display/models.py +6 -0
  111. wbcore/metadata/configs/fields.py +6 -1
  112. wbcore/metadata/configs/filter_fields.py +12 -11
  113. wbcore/models/fields.py +2 -2
  114. wbcore/permissions/permissions.py +2 -2
  115. wbcore/permissions/utils.py +2 -2
  116. wbcore/reversion/viewsets/titles.py +4 -3
  117. wbcore/serializers/__init__.py +1 -0
  118. wbcore/serializers/fields/__init__.py +1 -0
  119. wbcore/serializers/fields/datetime.py +35 -6
  120. wbcore/serializers/fields/fields.py +1 -1
  121. wbcore/serializers/fields/fsm.py +1 -1
  122. wbcore/serializers/fields/list.py +1 -1
  123. wbcore/serializers/fields/mixins.py +13 -5
  124. wbcore/serializers/fields/related.py +4 -6
  125. wbcore/serializers/fields/text.py +1 -1
  126. wbcore/serializers/fields/types.py +1 -0
  127. wbcore/serializers/serializers.py +6 -2
  128. wbcore/tasks.py +2 -2
  129. wbcore/templates/wbcore/email_base_template.html +3 -3
  130. wbcore/test/e2e_helpers_methods/e2e_checks.py +10 -4
  131. wbcore/test/e2e_helpers_methods/e2e_helper_methods.py +4 -2
  132. wbcore/test/mixins.py +1 -1
  133. wbcore/test/tests.py +6 -9
  134. wbcore/test/utils.py +3 -4
  135. wbcore/tests/e2e/test_e2e.py +2 -2
  136. wbcore/tests/test_cache/test_decorators.py +3 -3
  137. wbcore/tests/test_configs.py +1 -1
  138. wbcore/tests/test_fields/test_number_fields.py +1 -1
  139. wbcore/tests/test_filters/test_mixins.py +3 -3
  140. wbcore/tests/test_models/test_mixins.py +1 -1
  141. wbcore/tests/test_utils/test_date.py +1 -1
  142. wbcore/tests/test_utils/test_date_builder.py +25 -1
  143. wbcore/utils/date.py +18 -2
  144. wbcore/utils/figures.py +2 -2
  145. wbcore/utils/models.py +3 -2
  146. wbcore/utils/reportlab.py +7 -0
  147. wbcore/utils/rrules.py +1 -1
  148. wbcore/utils/string_loader.py +1 -1
  149. wbcore/utils/strings.py +2 -2
  150. wbcore/viewsets/mixins.py +6 -4
  151. {wbcore-1.54.10.dist-info → wbcore-1.58.2.dist-info}/METADATA +2 -1
  152. {wbcore-1.54.10.dist-info → wbcore-1.58.2.dist-info}/RECORD +153 -151
  153. {wbcore-1.54.10.dist-info → wbcore-1.58.2.dist-info}/WHEEL +0 -0
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, name, bases, dct):
27
- _class = super().__new__(cls, name, bases, dct)
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
- setattr(_class, "FSM_BUTTONS", getattr(_class, "FSM_BUTTONS", set()))
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
- html = "<ul>" + "".join(f"<li>{e}</li>" for e in warnings) + "</ul>"
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
- assert self.label or self.icon, "Either label or icon has to be defined."
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
- assert hasattr(self, "button_type"), "button_type cannot be None."
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
- assert bool(self.key) != bool(self.endpoint), "Either key or endpoint has to be defined. (Not both)"
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
- assert isinstance(self.buttons, tuple)
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(zip([None] * len(base_buttons), base_buttons)) # append an empty perfix for base buttons
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 (FSM_BUTTONS := getattr(self.view, "FSM_BUTTONS")) and len(FSM_BUTTONS) > 0:
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(FSM_BUTTONS),
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
- assert isinstance(self.value, bool), f"{Operator.EXISTS.value} is only compatible with bool"
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
- assert self.style, "style cannot both be None."
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
- assert all(
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
- if not self.key and len(self.children) == 0:
63
- self.key = slugify(str(self.label)) # we cast to str explicitly in case label is in a translation wrapper
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.key if parent_identifier else self.key
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
- assert all(
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.parent = filterset
29
- if res := field.get_representation(self.request, name, self.view):
30
- representation, lookup_expr = res
31
- if name in hidden_fields:
32
- lookup_expr["hidden"] = True
33
- if field.key in filter_fields:
34
- filter_fields[field.key]["lookup_expr"].append(lookup_expr)
35
- else:
36
- filter_fields[field.key] = representation
37
- filter_fields[field.key]["lookup_expr"] = [lookup_expr]
38
- filter_fields[field.key]["label"] = field.label
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=list(), **kwargs):
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 BasePermission
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(BasePermission):
43
+ class IsInternalUser(IsAuthenticated):
44
44
  def has_permission(self, request, view) -> bool:
45
45
  return is_internal_user(request.user, True)
46
46
 
@@ -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
- try:
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")
@@ -8,6 +8,7 @@ from .fields import (
8
8
  ColorPickerField,
9
9
  DateField,
10
10
  DateRangeField,
11
+ TimeRange,
11
12
  DateTimeField,
12
13
  DateTimeRangeField,
13
14
  DecimalField,
@@ -7,6 +7,7 @@ from .datetime import (
7
7
  DateRangeField,
8
8
  DateTimeField,
9
9
  DateTimeRangeField,
10
+ TimeRange,
10
11
  DurationField,
11
12
  TimeField,
12
13
  TimeZoneField,
@@ -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
- setattr(btn, "prefix_key", prefix)
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):
@@ -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, *args, **kwargs)
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
- assert position in ("left", "right"), "Decorator Position can only be right or left"
14
- assert decorator_type in ("icon", "text"), "Decorator Type can only be icon or text"
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
- try:
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
- setattr(self.child_relation, "queryset", self.child_relation._queryset)
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.SELECT.value)
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
- setattr(self, "queryset", self._queryset)
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
- try:
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
 
@@ -14,6 +14,7 @@ class WBCoreType(Enum):
14
14
  DATETIMERANGE = "datetimerange"
15
15
  DATE = "date"
16
16
  DATERANGE = "daterange"
17
+ TIMERANGE = "timerange"
17
18
  DURATION = "duration"
18
19
  TIME = "time"
19
20
  PRIMARY_KEY = "primary_key"
@@ -49,8 +49,8 @@ def validate_nested_representation(instance, value):
49
49
 
50
50
 
51
51
  class WBCoreSerializerMetaClass(SerializerMetaclass):
52
- def __new__(cls, name, bases, dct):
53
- _class = super().__new__(cls, name, bases, dct)
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
- BULK_SIZE = 1000
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) % BULK_SIZE == 0:
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: 500px;
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: 30px;
252
- padding-left: 30px;
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,