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
@@ -15,9 +15,9 @@ def cache_table(
15
15
  periodic_caching_get_parameters: list[dict[str, str]] | Callable | None = None,
16
16
  ):
17
17
  def _decorator(pandas_view_class):
18
- setattr(pandas_view_class, "CACHE_ENABLED", not settings.DEBUG)
19
- setattr(pandas_view_class, "CACHE_TIMEOUT", timeout)
20
- setattr(pandas_view_class, "CACHE_KEY_PREFIX", key_prefix)
18
+ pandas_view_class.CACHE_ENABLED = not settings.DEBUG
19
+ pandas_view_class.CACHE_TIMEOUT = timeout
20
+ pandas_view_class.CACHE_KEY_PREFIX = key_prefix
21
21
  add_extra_button.connect(add_clear_cache_button, sender=pandas_view_class, weak=False)
22
22
 
23
23
  if periodic_caching:
wbcore/cache/registry.py CHANGED
@@ -61,8 +61,9 @@ class CachedClass:
61
61
  for kwargs in view_kwargs:
62
62
  for request in self._get_requests(**kwargs):
63
63
  with suppress(RequiredFilterMissing):
64
- view = self.view_class(request=request, kwargs=kwargs)
65
- setattr(request, "parser_context", {"request": request, "view": view, "kwargs": kwargs})
64
+ view = self.view_class()
65
+ view.setup(request, **kwargs)
66
+ request.parser_context = {"request": request, "view": view, "kwargs": kwargs}
66
67
  cache_key = view._get_cache_key()
67
68
  cache.delete(cache_key)
68
69
  res.append(view._get_dataframe())
@@ -7,5 +7,5 @@ def register_config(func: Callable) -> Callable:
7
7
  of all configs. This function should only be used in a module called configs, otherwise the auto-discover
8
8
  functionality won't work.
9
9
  """
10
- setattr(func, "_is_config", True)
10
+ func._is_config = True
11
11
  return func
@@ -39,13 +39,13 @@ class Apps:
39
39
  return getattr(self, "ADDITIONAL_APPS", []) + getattr(self, "WB_ENDPOINTS", [])
40
40
 
41
41
  @property
42
- def INSTALLED_APPS(self):
42
+ def INSTALLED_APPS(self): # noqa
43
43
  return self._BASE_APPS + self._get_additional_and_wb_apps()
44
44
 
45
45
 
46
46
  class DevApps(Apps):
47
47
  @property
48
- def INSTALLED_APPS(self):
48
+ def INSTALLED_APPS(self): # noqa
49
49
  apps = self._BASE_APPS
50
50
 
51
51
  if self.DEBUG:
@@ -24,7 +24,7 @@ class Authentication:
24
24
  JWT_COOKIE_KEY = values.Value("JWT-access", environ_prefix=None)
25
25
 
26
26
  @property
27
- def SIMPLE_JWT(self):
27
+ def SIMPLE_JWT(self): # noqa
28
28
  return {
29
29
  "ACCESS_TOKEN_LIFETIME": timedelta(seconds=self.JWT_ACCESS_TOKEN_LIFETIME),
30
30
  "REFRESH_TOKEN_LIFETIME": timedelta(seconds=self.JWT_REFRESH_TOKEN_LIFETIME),
@@ -9,7 +9,7 @@ class Base:
9
9
  SITE_ID = values.IntegerValue(1, environ_prefix=None)
10
10
 
11
11
  @property
12
- def PROJECT_NAME(self):
12
+ def PROJECT_NAME(self): # noqa
13
13
  if settings_module := os.environ.get("DJANGO_SETTINGS_MODULE", None):
14
14
  return settings_module.split(".")[0]
15
15
  return None
@@ -8,7 +8,7 @@ class Cache:
8
8
  ) # The default (10 days) timeout in seconds
9
9
 
10
10
  @property
11
- def CACHES(self):
11
+ def CACHES(self): # noqa
12
12
  if self.CACHE_REDIS_URL:
13
13
  return {
14
14
  "default": {
@@ -6,7 +6,7 @@ class Maintenance:
6
6
  MAINTENANCE_MODE_STR: str | None = values.Value("False", environ_prefix=None)
7
7
 
8
8
  # We allow maintenance mode to be either True, False or None. In case of None, the specified Backend will take over.
9
- def MAINTENANCE_MODE(self) -> bool | None:
9
+ def MAINTENANCE_MODE(self) -> bool | None: # noqa
10
10
  if value := self.MAINTENANCE_MODE_STR:
11
11
  normalized_value = value.strip().lower()
12
12
  if normalized_value in values.BooleanValue.true_values:
@@ -7,7 +7,7 @@ class Media:
7
7
 
8
8
  class LocalMedia(Media):
9
9
  @property
10
- def MEDIA_ROOT(self):
10
+ def MEDIA_ROOT(self): # noqa
11
11
  return self.BASE_DIR.joinpath("mediafiles")
12
12
 
13
13
 
@@ -14,7 +14,7 @@ class Middleware:
14
14
 
15
15
  class DevMiddleware:
16
16
  @property
17
- def MIDDLEWARE(self):
17
+ def MIDDLEWARE(self): # noqa
18
18
  middleware = []
19
19
  middleware_list = Middleware.MIDDLEWARE.copy()
20
20
  if self.DEBUG:
@@ -6,7 +6,7 @@ class Restframework:
6
6
  REST_FRAMEWORK_THROTTLE_PERIOD = values.Value("hour", environ_prefix=None)
7
7
  REST_FRAMEWORK_USER_THROTTLE_RATES = values.IntegerValue(5000, environ_prefix=None)
8
8
 
9
- def REST_FRAMEWORK(self):
9
+ def REST_FRAMEWORK(self): # noqa
10
10
  rest_framework = {
11
11
  "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination",
12
12
  "PAGE_SIZE": 25,
@@ -13,7 +13,7 @@ class LocalStaticfiles(Staticfiles):
13
13
  }
14
14
 
15
15
  @property
16
- def STATIC_ROOT(self):
16
+ def STATIC_ROOT(self): # noqa
17
17
  return self.BASE_DIR.joinpath("staticfiles")
18
18
 
19
19
 
@@ -46,7 +46,7 @@ class WBCore:
46
46
  MESSAGE_STORAGE = "wbcore.messages.route_message_storage"
47
47
 
48
48
  @property
49
- def FRONTEND_CONTEXT(self):
49
+ def FRONTEND_CONTEXT(self): # noqa
50
50
  base_url = f"{self.CDN_BASE_ENDPOINT_URL}/{self.FRONTEND_VERSION}/"
51
51
  return {
52
52
  "TITLE": "Workbench",
@@ -54,7 +54,7 @@ class ContentTypeRepresentationSerializer(serializers.RepresentationSerializer):
54
54
  fields = ("id", "app_label", "model", "model_title")
55
55
 
56
56
 
57
- class GenericModel(models.Model):
57
+ class GenericModel(models.Model): # noqa
58
58
  id = models.IntegerField(primary_key=True)
59
59
  label = models.CharField(max_length=256)
60
60
 
@@ -1,3 +1,5 @@
1
+ from contextlib import suppress
2
+
1
3
  from django.contrib.contenttypes.models import ContentType
2
4
 
3
5
 
@@ -17,12 +19,10 @@ def get_ancestors_content_type(content_type):
17
19
  ]
18
20
  """
19
21
  for _class in content_type.model_class().__mro__:
20
- try:
22
+ with suppress(Exception):
21
23
  ancestor_content_type = ContentType.objects.get_for_model(_class)
22
24
  if ancestor_content_type.model_class():
23
25
  yield ancestor_content_type
24
- except Exception:
25
- pass
26
26
 
27
27
 
28
28
  def get_view_content_type_id(view):
@@ -38,13 +38,13 @@ class CalendarItemRepresentationViewSet(viewsets.RepresentationViewSet):
38
38
 
39
39
 
40
40
  class CalendarItemMetaClass(type(viewsets.ModelViewSet)):
41
- def __new__(metacls, cls, bases, classdict, **kwds):
42
- _class = super().__new__(metacls, cls, bases, classdict, **kwds)
41
+ def __new__(cls, *args, **kwargs):
42
+ _class = super().__new__(cls, *args, **kwargs)
43
43
  dependant_identifiers = []
44
44
  for subclass in get_inheriting_subclasses(_class):
45
45
  if identifier := getattr(subclass, "IDENTIFIER", None):
46
46
  dependant_identifiers.append(identifier)
47
- setattr(_class, "DEPENDANT_IDENTIFIERS", dependant_identifiers)
47
+ _class.DEPENDANT_IDENTIFIERS = dependant_identifiers
48
48
  return _class
49
49
 
50
50
 
@@ -68,7 +68,7 @@ class CalendarItemViewSet(viewsets.ModelViewSet, metaclass=CalendarItemMetaClass
68
68
  Property to store the unioned queryset of draggable calendar items. Each module inheriting from CalendarItem are expected to define the signal receiver
69
69
  """
70
70
  qs = CalendarItem.objects.none()
71
- for tmp, union_qs in draggable_calendar_item_ids.send(CalendarItem, request=self.request):
71
+ for _, union_qs in draggable_calendar_item_ids.send(CalendarItem, request=self.request):
72
72
  qs = qs.union(union_qs)
73
73
  return qs
74
74
 
@@ -154,15 +154,15 @@ def get_ics(request):
154
154
  min_max_range = TimestamptzRange(minimum_date, maximum_date)
155
155
  gen = qs.filter(period__contained_by=min_max_range)
156
156
 
157
- iCal = Calendar()
157
+ ical = Calendar()
158
158
  for occurence in gen:
159
159
  start = occurence.period.lower
160
160
  end = occurence.period.upper
161
161
  activity = occurence
162
162
  event = activity.to_ics(start, end)
163
163
  if event:
164
- iCal.events.add(event)
165
- data = StringIO(str(iCal))
164
+ ical.events.add(event)
165
+ data = StringIO(str(ical))
166
166
  response = HttpResponse(data, content_type="text/calendar")
167
167
  response["Content-Disposition"] = f'attachment; filename="{profile.computed_str}.ics"'
168
168
  return response
@@ -7,12 +7,12 @@ from django.db.models.signals import ModelSignal
7
7
  from langchain_core.language_models import BaseChatModel
8
8
  from langchain_core.messages import BaseMessage
9
9
  from langchain_openai import ChatOpenAI
10
+ from pydantic import BaseModel
10
11
 
11
12
  from ..exceptions import APIStatusErrors, BadRequestErrors
12
13
  from .utils import run_llm
13
14
 
14
15
  logger = logging.getLogger("llm")
15
- from pydantic import BaseModel
16
16
 
17
17
 
18
18
  @shared_task(
@@ -202,11 +202,11 @@ class UserAdmin(admin.ModelAdmin):
202
202
  form = self.change_password_form(user)
203
203
 
204
204
  fieldsets = [(None, {"fields": list(form.base_fields)})]
205
- adminForm = admin.helpers.AdminForm(form, fieldsets, {})
205
+ admin_form = admin.helpers.AdminForm(form, fieldsets, {})
206
206
 
207
207
  context = {
208
208
  "title": _("Change password: %s") % escape(user.get_username()),
209
- "adminForm": adminForm,
209
+ "adminForm": admin_form,
210
210
  "form_url": form_url,
211
211
  "form": form,
212
212
  "is_popup": (IS_POPUP_VAR in request.POST or IS_POPUP_VAR in request.GET),
@@ -6,7 +6,6 @@ from .models import UserActivity
6
6
 
7
7
  class UserActivityChartFilter(wb_filters.FilterSet):
8
8
  date = wb_filters.DateTimeRangeFilter(
9
- method=wb_filters.DateRangeFilter.base_date_range_filter_method,
10
9
  label="Date Range",
11
10
  required=True,
12
11
  clearable=False,
@@ -166,7 +166,7 @@ class User(AbstractBaseUser, PermissionsMixin):
166
166
  return self.profile.last_name
167
167
 
168
168
  @classmethod
169
- def get_endpoint_basename(self):
169
+ def get_endpoint_basename(cls):
170
170
  return "wbcore:authentication:user"
171
171
 
172
172
  @classmethod
@@ -187,7 +187,7 @@ class Group(DjangoBaseGroup):
187
187
  proxy = True
188
188
 
189
189
  @classmethod
190
- def get_endpoint_basename(self):
190
+ def get_endpoint_basename(cls):
191
191
  return "wbcore:authentication:group"
192
192
 
193
193
  @classmethod
@@ -208,7 +208,7 @@ class Permission(DjangoBasePermission):
208
208
  proxy = True
209
209
 
210
210
  @classmethod
211
- def get_endpoint_basename(self):
211
+ def get_endpoint_basename(cls):
212
212
  return "wbcore:authentication:permission"
213
213
 
214
214
  @classmethod
@@ -59,7 +59,7 @@ class UserActivity(models.Model):
59
59
  return Subquery(qs.order_by("-latest_refresh").values("latest_refresh")[:1], output_field=DateTimeField())
60
60
 
61
61
  @classmethod
62
- def get_endpoint_basename(self):
62
+ def get_endpoint_basename(cls):
63
63
  return "wbcore:authentication:useractivity"
64
64
 
65
65
  @classmethod
@@ -60,7 +60,7 @@ class ChangePasswordSerializer(wb_serializers.Serializer):
60
60
  validate_password(new_password)
61
61
  return super().validate(data)
62
62
  except serializers.ValidationError as e:
63
- raise serializers.ValidationError({"new_password": e, "confirm_password": e})
63
+ raise serializers.ValidationError({"new_password": e, "confirm_password": e}) from e
64
64
 
65
65
  class Meta:
66
66
  fields = ("current_password", "new_password", "confirm_password")
@@ -336,7 +336,7 @@ class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
336
336
  raise exceptions.AuthenticationFailed(
337
337
  self.error_messages["no_active_account"],
338
338
  "no_active_account",
339
- )
339
+ ) from None
340
340
  return data
341
341
 
342
342
 
@@ -9,7 +9,7 @@ from rest_framework import exceptions
9
9
  from rest_framework.test import APIRequestFactory
10
10
 
11
11
  from wbcore.contrib.authentication.authentication import inject_short_lived_token
12
- from wbcore.contrib.authentication.models import Token
12
+ from wbcore.contrib.authentication.models import Token, User
13
13
 
14
14
  fake = Faker()
15
15
  now = datetime.now()
@@ -17,9 +17,9 @@ now = datetime.now()
17
17
 
18
18
  class TestTokenUnitTests:
19
19
  @pytest.fixture
20
- def request_with_user(user):
20
+ def request_with_user(self: User):
21
21
  request = APIRequestFactory().get("/")
22
- request.user = user
22
+ request.user = self
23
23
  return request
24
24
 
25
25
  def setup_method(self):
@@ -155,7 +155,6 @@ class TestRegistrationAndActivationViews:
155
155
  # Arrange
156
156
  mock_get_user = mocker.patch(
157
157
  "wbcore.contrib.authentication.models.users.UserManager.get",
158
- side_effect=Exception if should_fail else None,
159
158
  return_value=None if should_fail else test_user,
160
159
  )
161
160
  mock_reset_password = mocker.patch.object(test_user, "reset_password")
@@ -157,7 +157,8 @@ class UserActivityChart(viewsets.ChartViewSet):
157
157
  cum_times = pd.Series(
158
158
  dt
159
159
  for group in [
160
- pd.date_range(start, end, freq="Min") for start, end in zip(dff.date, dff.latest_refresh)
160
+ pd.date_range(start, end, freq="Min")
161
+ for start, end in zip(dff.date, dff.latest_refresh, strict=False)
161
162
  ]
162
163
  for dt in group
163
164
  ).value_counts()
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ from contextlib import suppress
2
3
 
3
4
  from django.conf import settings
4
5
  from django.contrib.auth.models import Group, Permission
@@ -66,11 +67,12 @@ logger = logging.getLogger()
66
67
  @permission_classes([AllowAny])
67
68
  @authentication_classes([])
68
69
  def reset_password_email(request):
69
- try:
70
+ with suppress(User.DoesNotExist):
70
71
  user = User.objects.get(email=request.data["email"])
71
- user.reset_password(request)
72
- except Exception:
73
- pass
72
+ try:
73
+ user.reset_password(request)
74
+ except Exception as e:
75
+ logger.error(f"While user {user} try to reset password, we encounter the error {e}")
74
76
  return Response(
75
77
  {
76
78
  "status": "ok",
@@ -39,7 +39,8 @@ class ColorGradient(models.Model):
39
39
  if color:
40
40
  color_rgb = _hex2rgb(color)
41
41
  nearest_color = min(
42
- self.colors, key=lambda subject: sum((s - q) ** 2 for s, q in zip(_hex2rgb(subject), color_rgb))
42
+ self.colors,
43
+ key=lambda subject: sum((s - q) ** 2 for s, q in zip(_hex2rgb(subject), color_rgb, strict=False)),
43
44
  )
44
45
 
45
46
  index = self.colors.index(nearest_color)
@@ -6,7 +6,7 @@ import yaml
6
6
  from .models import Currency, CurrencyFXRates
7
7
 
8
8
  with open(pathlib.Path(__file__).parent.joinpath("fixtures").joinpath("currency.yaml"), "r") as yaml_file:
9
- currency_dict = yaml.load(yaml_file, Loader=yaml.CLoader)
9
+ currency_dict = yaml.load(yaml_file, Loader=yaml.CLoader) # noqa: S506
10
10
 
11
11
 
12
12
  class CurrencyFactory(factory.django.DjangoModelFactory):
@@ -55,7 +55,9 @@ class DataBackend(AbstractDataBackend):
55
55
  params["symbols"] = ",".join(obj_external_ids)
56
56
  res = []
57
57
  for _date in pd.date_range(start, execution_date, freq="B"):
58
- r = requests.get(f'{self.ENDPOINT}/{_date.strftime("%Y-%m-%d")}?', params=params, headers=self.HEADERS)
58
+ r = requests.get(
59
+ f'{self.ENDPOINT}/{_date.strftime("%Y-%m-%d")}?', params=params, headers=self.HEADERS, timeout=10
60
+ )
59
61
  if r.status_code == requests.codes.ok:
60
62
  res_json = r.json()
61
63
  if res_json and res_json["success"]:
@@ -4,7 +4,9 @@ from datetime import date as date_lib
4
4
  from decimal import Decimal
5
5
 
6
6
  from django.db import models
7
- from django.db.models import Expression, ExpressionWrapper, Subquery
7
+ from django.db.models import Case, CharField, Expression, ExpressionWrapper, F, Q, Subquery, Value, When
8
+ from django.db.models.functions import Concat
9
+ from persisting_theory import QuerySet
8
10
 
9
11
  from wbcore.contrib.io.mixins import ImportMixin
10
12
  from wbcore.models import WBModel
@@ -13,6 +15,21 @@ from wbcore.utils.models import LabelKeyMixin
13
15
  from .import_export.handlers import CurrencyFXRatesImportHandler, CurrencyImportHandler
14
16
 
15
17
 
18
+ class CurrencyDefaultManager(models.Manager):
19
+ def get_queryset(self) -> QuerySet:
20
+ return (
21
+ super()
22
+ .get_queryset()
23
+ .annotate(
24
+ name_repr=Case(
25
+ When(Q(symbol="") | Q(symbol__isnull=True), then=F("title")),
26
+ default=Concat(F("title"), Value(" ("), F("symbol"), Value(")")),
27
+ output_field=CharField(),
28
+ )
29
+ )
30
+ )
31
+
32
+
16
33
  class Currency(ImportMixin, LabelKeyMixin, WBModel):
17
34
  import_export_handler_class = CurrencyImportHandler
18
35
 
@@ -21,18 +38,19 @@ class Currency(ImportMixin, LabelKeyMixin, WBModel):
21
38
  verbose_name_plural = "Currencies"
22
39
  ordering = ("title",)
23
40
 
24
- def __str__(self):
25
- symbol = f"({self.symbol})" if self.symbol else ""
26
- return f"{self.key} {symbol}"
41
+ def __str__(self) -> str:
42
+ if self.symbol:
43
+ return f"{self.title} ({self.symbol})"
44
+ return self.title
27
45
 
28
- def __repr__(self):
46
+ def __repr__(self) -> str:
29
47
  return self.key
30
48
 
31
49
  title = models.CharField(max_length=255)
32
50
  symbol = models.CharField(max_length=10, blank=True, null=True)
33
51
  key = models.CharField(max_length=3, unique=True)
34
52
 
35
- objects = models.Manager()
53
+ objects = CurrencyDefaultManager()
36
54
 
37
55
  LABEL_KEY = "{{key}} ({{symbol}})"
38
56
 
@@ -65,7 +83,9 @@ class Currency(ImportMixin, LabelKeyMixin, WBModel):
65
83
  raise CurrencyFXRates.DoesNotExist
66
84
  return (1 / base.value) * other.value
67
85
  except CurrencyFXRates.DoesNotExist:
68
- return Decimal(1.0)
86
+ if exact_lookup:
87
+ return self.convert(valuation_date, other_currency, exact_lookup=False)
88
+ raise CurrencyFXRates.DoesNotExist from None
69
89
 
70
90
  @classmethod
71
91
  def get_endpoint_basename(cls) -> str:
@@ -81,7 +101,7 @@ class Currency(ImportMixin, LabelKeyMixin, WBModel):
81
101
 
82
102
  @classmethod
83
103
  def get_representation_label_key(cls) -> str:
84
- return "{{key}} ({{symbol}})"
104
+ return "{{name_repr}}"
85
105
 
86
106
 
87
107
  class CurrencyFXRates(ImportMixin, models.Model):
@@ -8,11 +8,15 @@ from .models import Currency, CurrencyFXRates
8
8
  class CurrencyRepresentationSerializer(wb_serializers.RepresentationSerializer):
9
9
  """Representation Serializer for Currencies"""
10
10
 
11
+ name_repr = wb_serializers.SerializerMethodField(read_only=True)
11
12
  _detail = wb_serializers.HyperlinkField(reverse_name="wbcore:currency:currency-detail")
12
13
 
14
+ def get_name_repr(self, obj):
15
+ return str(obj)
16
+
13
17
  class Meta:
14
18
  model = Currency
15
- fields = ("id", "title", "key", "symbol", "_detail")
19
+ fields = ("id", "name_repr", "key", "_detail")
16
20
 
17
21
 
18
22
  class CurrencyModelSerializer(wb_serializers.ModelSerializer):
@@ -30,6 +30,10 @@ class TestSerializers:
30
30
  mocker.patch.object(currency_fx_rate, "currency_id", currency.id)
31
31
  return currency_fx_rate
32
32
 
33
+ @pytest.fixture
34
+ def expected_representation_data(self, currency):
35
+ return {"id": currency.id, "name_repr": f"{currency.title} ({currency.symbol})", "key": currency.key}
36
+
33
37
  @pytest.fixture
34
38
  def expected_data(self, currency):
35
39
  return {"id": currency.id, "title": currency.title, "key": currency.key, "symbol": currency.symbol}
@@ -43,12 +47,12 @@ class TestSerializers:
43
47
  "currency": currency_fx_rate.currency_id,
44
48
  }
45
49
 
46
- def test_currency_representation_serializer(self, mocker: MockerFixture, currency, expected_data):
50
+ def test_currency_representation_serializer(self, mocker: MockerFixture, currency, expected_representation_data):
47
51
  detail = f"/wbcore/currency/currency/{currency.id}/"
48
52
  mocker.patch("wbcore.serializers.fields.fields.HyperlinkField", return_value=detail)
49
53
  serializer = CurrencyRepresentationSerializer(instance=currency)
50
- expected_data["_detail"] = detail
51
- assert serializer.data == expected_data
54
+ expected_representation_data["_detail"] = detail
55
+ assert serializer.data == expected_representation_data
52
56
 
53
57
  def test_currency_model_serializer(self, mocker: MockerFixture, currency, expected_data):
54
58
  mocker.patch(
@@ -50,7 +50,7 @@ class TestCurrencyRepresentationViewSets:
50
50
  assert response.status_code == status.HTTP_200_OK
51
51
  assert instance.get("id") == euro.id
52
52
  assert instance.get("key") == euro.key
53
- assert instance.get("title") == euro.title
53
+ assert instance.get("name_repr") == f"{euro.title} ({euro.symbol})"
54
54
 
55
55
 
56
56
  @pytest.mark.viewset_tests
@@ -19,8 +19,8 @@ class CurrencyRepresentationViewSet(viewsets.RepresentationViewSet):
19
19
  serializer_class = CurrencyRepresentationSerializer
20
20
  filter_backends = (filters.OrderingFilter, filters.SearchFilter)
21
21
 
22
- ordering = ordering_fields = ["-symbol"]
23
- search_fields = ("symbol", "key")
22
+ ordering = ["name_repr"]
23
+ search_fields = ("name_repr", "key")
24
24
 
25
25
 
26
26
  class CurrencyModelViewSet(viewsets.ModelViewSet):
@@ -12,9 +12,9 @@ def dictfetchall[T](cursor: "CursorWrapper", dict_type: type[T] = dict) -> Itera
12
12
  columns = get_columns(cursor)
13
13
  for row in cursor.fetchall():
14
14
  # The spec for TypedDict is not compliant with the sepc for dict
15
- yield dict_type(zip(columns, row)) # type: ignore
15
+ yield dict_type(zip(columns, row, strict=False)) # type: ignore
16
16
 
17
17
 
18
18
  def dictfetchone(cursor: "CursorWrapper") -> dict:
19
19
  columns = get_columns(cursor)
20
- return dict(zip(columns, cursor.fetchone() or []))
20
+ return dict(zip(columns, cursor.fetchone() or [], strict=False))
@@ -18,7 +18,7 @@ from .entries import (
18
18
  EntryFactory,
19
19
  PersonFactory,
20
20
  PersonWithEmployerFactory,
21
- Random_ClientFactory,
21
+ RandomClientFactory,
22
22
  SpecializationFactory,
23
23
  UnemployedPersonFactory,
24
24
  PersonSignatureFactory,
@@ -158,7 +158,7 @@ class ClientFactory(PersonFactory):
158
158
  self.clients.add(person)
159
159
 
160
160
 
161
- class Random_ClientFactory(ClientFactory):
161
+ class RandomClientFactory(ClientFactory):
162
162
  @factory.post_generation
163
163
  def clients(self, create, extracted, **kwargs):
164
164
  self.clients.add(PersonFactory())
@@ -40,7 +40,7 @@ class BankingContact(PrimaryMixin, WBModel):
40
40
  @classmethod
41
41
  def get_color_map(cls):
42
42
  colors = [WBColor.RED_LIGHT.value, WBColor.YELLOW_LIGHT.value, WBColor.GREEN_LIGHT.value]
43
- return [choice for choice in zip(cls, colors)]
43
+ return [choice for choice in zip(cls, colors, strict=False)]
44
44
 
45
45
  status = FSMField(
46
46
  default=Status.DRAFT,
@@ -227,7 +227,7 @@ class BankingContact(PrimaryMixin, WBModel):
227
227
 
228
228
  notification_types = [
229
229
  create_notification_type(
230
- code="directory.banking+contact.approval",
230
+ code="directory.banking_contact.approval",
231
231
  title="Banking Contact Notification",
232
232
  help_text="Sends out a notification when you want need to approve a change in bank details",
233
233
  )