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/cache/decorators.py
CHANGED
|
@@ -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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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(
|
|
65
|
-
|
|
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())
|
wbcore/configs/decorators.py
CHANGED
|
@@ -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
|
|
@@ -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:
|
|
@@ -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,
|
|
@@ -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
|
|
wbcore/content_type/utils.py
CHANGED
|
@@ -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
|
-
|
|
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__(
|
|
42
|
-
_class = super().__new__(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
165
|
-
data = StringIO(str(
|
|
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
|
wbcore/contrib/ai/llm/config.py
CHANGED
|
@@ -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
|
-
|
|
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":
|
|
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),
|
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
20
|
+
def request_with_user(self: User):
|
|
21
21
|
request = APIRequestFactory().get("/")
|
|
22
|
-
request.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")
|
|
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
|
-
|
|
70
|
+
with suppress(User.DoesNotExist):
|
|
70
71
|
user = User.objects.get(email=request.data["email"])
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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",
|
wbcore/contrib/color/models.py
CHANGED
|
@@ -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,
|
|
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(
|
|
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
|
-
|
|
26
|
-
|
|
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 =
|
|
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
|
-
|
|
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 "{{
|
|
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", "
|
|
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,
|
|
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
|
-
|
|
51
|
-
assert serializer.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("
|
|
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 =
|
|
23
|
-
search_fields = ("
|
|
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))
|
|
@@ -158,7 +158,7 @@ class ClientFactory(PersonFactory):
|
|
|
158
158
|
self.clients.add(person)
|
|
159
159
|
|
|
160
160
|
|
|
161
|
-
class
|
|
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.
|
|
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
|
)
|