wbcore 1.46.0__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 +5 -3
- wbcore/cache/registry.py +14 -7
- wbcore/configs/__init__.py +1 -0
- wbcore/configs/configs.py +5 -0
- wbcore/configs/decorators.py +1 -1
- wbcore/configurations/configurations/apps.py +3 -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/i18nl10n.py +2 -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 +3 -3
- wbcore/configurations/configurations/wbcore.py +1 -1
- wbcore/content_type/serializers.py +13 -5
- wbcore/content_type/utils.py +3 -3
- wbcore/content_type/viewsets.py +2 -2
- wbcore/contrib/agenda/filters/calendar_item.py +5 -4
- wbcore/contrib/agenda/locale/de/LC_MESSAGES/django.po +145 -52
- wbcore/contrib/agenda/locale/de/LC_MESSAGES/django.po.translated +236 -0
- wbcore/contrib/agenda/locale/en/LC_MESSAGES/django.po +200 -0
- wbcore/contrib/agenda/locale/fr/LC_MESSAGES/django.po +201 -0
- wbcore/contrib/agenda/viewsets/calendar_items.py +7 -7
- wbcore/contrib/agenda/viewsets/menu/calendar_items.py +0 -6
- wbcore/contrib/ai/exceptions.py +5 -5
- wbcore/contrib/ai/llm/config.py +76 -27
- wbcore/contrib/ai/llm/mixins.py +5 -8
- wbcore/contrib/ai/llm/utils.py +50 -26
- wbcore/contrib/authentication/admin.py +2 -2
- wbcore/contrib/authentication/factories/__init__.py +8 -1
- wbcore/contrib/authentication/factories/users.py +19 -0
- wbcore/contrib/authentication/filters.py +1 -2
- wbcore/contrib/authentication/locale/de/LC_MESSAGES/django.po +209 -187
- wbcore/contrib/authentication/locale/de/LC_MESSAGES/django.po.translated +634 -0
- wbcore/contrib/authentication/locale/en/LC_MESSAGES/django.po +590 -0
- wbcore/contrib/authentication/locale/fr/LC_MESSAGES/django.po +592 -0
- 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/urls.py +0 -4
- wbcore/contrib/authentication/viewsets/endpoints/user_activities.py +2 -11
- wbcore/contrib/authentication/viewsets/endpoints/users.py +0 -3
- 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 +30 -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/currency/viewsets/endpoints/currency_fx_rates.py +0 -9
- wbcore/contrib/dataloader/tests/test/dataloaders/protocols.py +1 -2
- wbcore/contrib/dataloader/utils.py +2 -2
- wbcore/contrib/directory/factories/__init__.py +1 -1
- wbcore/contrib/directory/factories/entries.py +2 -1
- wbcore/contrib/directory/filters/entries.py +9 -0
- wbcore/contrib/directory/locale/de/LC_MESSAGES/django.po +728 -714
- wbcore/contrib/directory/locale/de/LC_MESSAGES/django.po.translated +1779 -0
- wbcore/contrib/directory/locale/en/LC_MESSAGES/django.po +1652 -0
- wbcore/contrib/directory/locale/fr/LC_MESSAGES/django.po +1654 -0
- wbcore/contrib/directory/migrations/0011_person_description_person_i18n.py +24 -0
- wbcore/contrib/directory/migrations/0012_alter_person_managers.py +20 -0
- wbcore/contrib/directory/migrations/0013_alter_clientmanagerrelationship_options.py +17 -0
- wbcore/contrib/directory/models/contacts.py +2 -2
- wbcore/contrib/directory/models/entries.py +31 -5
- wbcore/contrib/directory/models/relationships.py +31 -35
- wbcore/contrib/directory/permissions.py +6 -0
- wbcore/contrib/directory/serializers/companies.py +16 -8
- wbcore/contrib/directory/serializers/contacts.py +8 -8
- wbcore/contrib/directory/serializers/entries.py +26 -15
- wbcore/contrib/directory/serializers/entry_representations.py +4 -2
- wbcore/contrib/directory/serializers/persons.py +12 -10
- 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/contacts.py +1 -14
- wbcore/contrib/directory/viewsets/display/entries.py +68 -38
- wbcore/contrib/directory/viewsets/display/relationships.py +26 -50
- wbcore/contrib/directory/viewsets/endpoints/relationships.py +1 -26
- wbcore/contrib/directory/viewsets/entries.py +8 -6
- 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/documents/locale/de/LC_MESSAGES/django.po +103 -94
- wbcore/contrib/documents/locale/de/LC_MESSAGES/django.po.translated +285 -0
- wbcore/contrib/documents/locale/en/LC_MESSAGES/django.po +271 -0
- wbcore/contrib/documents/locale/fr/LC_MESSAGES/django.po +270 -0
- wbcore/contrib/documents/tests/test_models.py +32 -28
- wbcore/contrib/documents/viewsets/endpoints/shareable_links.py +2 -21
- wbcore/contrib/dynamic_preferences/types.py +108 -0
- wbcore/contrib/dynamic_preferences/viewsets.py +27 -0
- wbcore/contrib/example_app/filters/event.py +3 -1
- wbcore/contrib/example_app/filters/match.py +1 -1
- wbcore/contrib/example_app/models.py +91 -22
- wbcore/contrib/example_app/serializers/person_team.py +4 -4
- wbcore/contrib/example_app/templates/example_app/embedded_view.html +19 -0
- wbcore/contrib/example_app/tests/e2e/test_teams.py +1 -1
- wbcore/contrib/example_app/tests/test_models/test_match.py +17 -7
- wbcore/contrib/example_app/urls.py +2 -0
- wbcore/contrib/example_app/views.py +7 -0
- wbcore/contrib/example_app/viewsets/displays/team.py +23 -4
- wbcore/contrib/example_app/viewsets/menu/menus.py +1 -1
- wbcore/contrib/example_app/viewsets/menus.py +1 -1
- wbcore/contrib/geography/tests/conftest.py +14 -0
- wbcore/contrib/geography/tests/test_models.py +23 -8
- wbcore/contrib/geography/tests/test_viewsets.py +96 -2
- wbcore/contrib/guardian/tests/test_model_mixins.py +3 -4
- wbcore/contrib/guardian/tests/test_tasks.py +9 -9
- wbcore/contrib/guardian/tests/test_viewsets.py +2 -2
- wbcore/contrib/guardian/viewsets/configs/__init__.py +1 -1
- wbcore/contrib/guardian/viewsets/configs/buttons.py +9 -0
- wbcore/contrib/guardian/viewsets/configs/endpoints.py +7 -0
- wbcore/contrib/guardian/viewsets/viewsets.py +2 -0
- wbcore/contrib/i18n/__init__.py +2 -0
- wbcore/contrib/i18n/buttons.py +33 -0
- wbcore/contrib/i18n/serializers/__init__.py +0 -0
- wbcore/contrib/i18n/serializers/fields.py +20 -0
- wbcore/contrib/i18n/serializers/mixins.py +13 -0
- wbcore/contrib/i18n/tests/conftest.py +11 -0
- wbcore/contrib/i18n/tests/test_viewsets.py +67 -0
- wbcore/contrib/i18n/translation.py +140 -0
- wbcore/contrib/i18n/viewsets.py +36 -0
- 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/admin.py +1 -0
- wbcore/contrib/io/backends/mail.py +3 -2
- wbcore/contrib/io/backends/utils.py +14 -17
- wbcore/contrib/io/exceptions.py +8 -0
- wbcore/contrib/io/factories.py +1 -1
- wbcore/contrib/io/import_export/backends/mail.py +1 -0
- wbcore/contrib/io/import_export/backends/sftp.py +29 -20
- wbcore/contrib/io/import_export/backends/stream.py +2 -2
- wbcore/contrib/io/import_export/parsers/__init__.py +0 -0
- wbcore/contrib/io/import_export/parsers/base_csv.py +36 -0
- wbcore/contrib/io/import_export/parsers/resources.py +50 -0
- wbcore/contrib/io/imports.py +33 -25
- wbcore/contrib/io/locale/de/LC_MESSAGES/django.po +114 -22
- wbcore/contrib/io/locale/de/LC_MESSAGES/django.po.translated +103 -0
- wbcore/contrib/io/locale/en/LC_MESSAGES/django.po +138 -0
- wbcore/contrib/io/locale/fr/LC_MESSAGES/django.po +138 -0
- wbcore/contrib/io/migrations/0008_importsource_resource_kwargs.py +18 -0
- wbcore/contrib/io/models.py +65 -45
- wbcore/contrib/io/resources.py +0 -6
- wbcore/contrib/io/serializers.py +2 -2
- wbcore/contrib/io/signals.py +4 -0
- wbcore/contrib/io/tests/test_backends.py +19 -13
- wbcore/contrib/io/tests/test_exports.py +1 -1
- wbcore/contrib/io/tests/test_imports.py +1 -1
- wbcore/contrib/io/tests/test_models.py +47 -14
- wbcore/contrib/io/tests/test_viewsets.py +271 -0
- wbcore/contrib/io/viewset_mixins.py +41 -54
- wbcore/contrib/notifications/admin.py +1 -0
- wbcore/contrib/notifications/apps.py +2 -1
- wbcore/contrib/notifications/backends/abstract_backend.py +2 -4
- wbcore/contrib/notifications/backends/firebase/backends.py +5 -2
- wbcore/contrib/notifications/dispatch.py +18 -7
- wbcore/contrib/notifications/factories/notification_types.py +1 -0
- wbcore/contrib/notifications/locale/de/LC_MESSAGES/django.po +25 -19
- wbcore/contrib/notifications/locale/de/LC_MESSAGES/django.po.translated +63 -0
- wbcore/contrib/notifications/locale/en/LC_MESSAGES/django.po +61 -0
- wbcore/contrib/notifications/locale/fr/LC_MESSAGES/django.po +62 -0
- wbcore/contrib/notifications/migrations/0008_notificationtype_is_lock.py +18 -0
- wbcore/contrib/notifications/migrations/0009_alter_notificationtypesetting_options_and_more.py +32 -0
- wbcore/contrib/notifications/models/notification_types.py +67 -24
- wbcore/contrib/notifications/serializers/notification_types.py +16 -1
- wbcore/contrib/notifications/tests/test_models/test_tokens.py +8 -0
- wbcore/contrib/notifications/tests/test_serializers/test_notification_types.py +5 -0
- wbcore/contrib/notifications/tests/test_viewsets/test_notification_types.py +3 -5
- wbcore/contrib/notifications/utils.py +3 -2
- wbcore/contrib/notifications/viewsets/configs/notification_types.py +28 -6
- wbcore/contrib/notifications/viewsets/menus.py +1 -1
- wbcore/contrib/notifications/viewsets/notification_types.py +12 -2
- wbcore/contrib/pandas/fields.py +38 -10
- wbcore/contrib/pandas/filters.py +4 -1
- wbcore/contrib/pandas/filterset.py +8 -7
- wbcore/contrib/pandas/tests/test_fields/test_number_fields.py +2 -7
- wbcore/contrib/pandas/utils.py +1 -1
- wbcore/contrib/pandas/views.py +14 -13
- wbcore/contrib/tags/models/tags.py +4 -1
- wbcore/contrib/workflow/factories/display.py +2 -2
- wbcore/contrib/workflow/factories/transition.py +16 -15
- wbcore/contrib/workflow/locale/de/LC_MESSAGES/django.po +457 -566
- wbcore/contrib/workflow/locale/de/LC_MESSAGES/django.po.translated +1326 -0
- wbcore/contrib/workflow/locale/en/LC_MESSAGES/django.po +1102 -0
- wbcore/contrib/workflow/locale/fr/LC_MESSAGES/django.po +1114 -0
- wbcore/contrib/workflow/models/data.py +7 -4
- wbcore/contrib/workflow/models/process.py +2 -2
- wbcore/contrib/workflow/models/step.py +57 -15
- wbcore/contrib/workflow/serializers/data.py +8 -8
- wbcore/contrib/workflow/serializers/process.py +3 -2
- wbcore/contrib/workflow/tests/conftest.py +224 -0
- wbcore/contrib/workflow/tests/test_dispatch.py +82 -77
- wbcore/contrib/workflow/tests/test_displays.py +10 -88
- wbcore/contrib/workflow/tests/test_filters.py +57 -40
- wbcore/contrib/workflow/tests/test_models/step/test_decision_step.py +71 -68
- wbcore/contrib/workflow/tests/test_models/step/test_email_step.py +78 -38
- wbcore/contrib/workflow/tests/test_models/step/test_finish_step.py +152 -90
- wbcore/contrib/workflow/tests/test_models/step/test_join_step.py +100 -110
- wbcore/contrib/workflow/tests/test_models/step/test_step.py +168 -33
- wbcore/contrib/workflow/tests/test_models/test_condition.py +1 -1
- wbcore/contrib/workflow/tests/test_models/test_workflow.py +3 -3
- wbcore/contrib/workflow/tests/test_serializers.py +172 -150
- wbcore/contrib/workflow/tests/test_viewsets.py +264 -323
- wbcore/contrib/workflow/tests/test_workflow_assignees.py +215 -205
- wbcore/contrib/workflow/viewsets/process.py +4 -1
- wbcore/contrib/workflow/workflows/assignees.py +12 -7
- wbcore/dynamic_preferences_registry.py +102 -0
- wbcore/enums.py +2 -51
- wbcore/filters/fields/choices.py +4 -6
- wbcore/filters/fields/content_type.py +15 -4
- wbcore/filters/fields/datetime.py +50 -25
- wbcore/filters/fields/models.py +18 -9
- wbcore/filters/fields/numbers.py +9 -8
- wbcore/filters/filterset.py +27 -6
- wbcore/filters/mixins.py +41 -42
- wbcore/forms.py +6 -6
- wbcore/fsm/markdown_extensions.py +1 -1
- wbcore/fsm/mixins.py +20 -6
- wbcore/locale/de/LC_MESSAGES/django.po +982 -397
- wbcore/locale/de/LC_MESSAGES/django.po.translated +1580 -0
- wbcore/locale/en/LC_MESSAGES/django.po +1234 -0
- wbcore/locale/fr/LC_MESSAGES/django.po +1235 -0
- wbcore/markdown/models.py +8 -5
- wbcore/markdown/views.py +1 -1
- wbcore/menus/menus.py +2 -2
- wbcore/metadata/configs/buttons/bases.py +10 -7
- wbcore/metadata/configs/buttons/buttons.py +2 -1
- wbcore/metadata/configs/buttons/enums.py +50 -0
- wbcore/metadata/configs/buttons/view_config.py +13 -46
- wbcore/metadata/configs/display/display.py +2 -2
- wbcore/metadata/configs/display/formatting.py +6 -9
- wbcore/metadata/configs/display/instance_display/display.py +5 -2
- wbcore/metadata/configs/display/instance_display/pages.py +1 -1
- wbcore/metadata/configs/display/instance_display/shortcuts.py +1 -1
- wbcore/metadata/configs/display/list_display.py +54 -40
- wbcore/metadata/configs/display/models.py +6 -0
- wbcore/metadata/configs/display/view_config.py +11 -9
- wbcore/metadata/configs/endpoints.py +11 -4
- wbcore/metadata/configs/fields.py +6 -1
- wbcore/metadata/configs/filter_fields.py +12 -13
- wbcore/metadata/configs/identifiers.py +3 -1
- wbcore/metadata/tests/test_buttons.py +13 -16
- wbcore/models/fields.py +2 -2
- wbcore/pagination.py +1 -2
- wbcore/permissions/permissions.py +2 -2
- wbcore/permissions/utils.py +2 -2
- wbcore/release_notes/display.py +2 -8
- wbcore/release_notes/serializers.py +2 -9
- wbcore/release_notes/viewsets.py +8 -2
- wbcore/reversion/viewsets/titles.py +4 -3
- wbcore/serializers/__init__.py +2 -0
- wbcore/serializers/fields/__init__.py +2 -1
- wbcore/serializers/fields/boolean.py +1 -1
- wbcore/serializers/fields/choice.py +28 -4
- wbcore/serializers/fields/datetime.py +45 -36
- wbcore/serializers/fields/fields.py +1 -1
- wbcore/serializers/fields/fsm.py +1 -1
- wbcore/serializers/fields/list.py +2 -5
- wbcore/serializers/fields/mixins.py +24 -11
- wbcore/serializers/fields/number.py +6 -23
- wbcore/serializers/fields/other.py +2 -10
- wbcore/serializers/fields/related.py +4 -6
- wbcore/serializers/fields/text.py +1 -1
- wbcore/serializers/fields/types.py +2 -0
- wbcore/serializers/serializers.py +12 -3
- wbcore/signals/__init__.py +1 -0
- wbcore/signals/clone.py +4 -0
- wbcore/signals/models.py +2 -6
- 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 +52 -102
- 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 +4 -7
- wbcore/tests/test_configs.py +2 -5
- wbcore/tests/test_enums.py +2 -1
- wbcore/tests/test_fields/test_choice_fields.py +9 -1
- wbcore/tests/test_fields/test_number_fields.py +7 -15
- wbcore/tests/test_fields/test_other_fields.py +1 -2
- wbcore/tests/test_filters/test_mixins.py +35 -35
- wbcore/tests/test_list_display.py +0 -2
- 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/tests/test_utils/test_primary.py +1 -1
- wbcore/urls.py +4 -0
- wbcore/utils/date.py +18 -2
- wbcore/utils/figures.py +2 -2
- wbcore/utils/models.py +21 -4
- wbcore/utils/reportlab.py +7 -0
- wbcore/utils/rrules.py +3 -1
- wbcore/utils/string_loader.py +1 -1
- wbcore/utils/strings.py +3 -3
- wbcore/utils/views.py +8 -3
- wbcore/viewsets/mixins.py +9 -4
- {wbcore-1.46.0.dist-info → wbcore-1.58.2.dist-info}/METADATA +9 -5
- {wbcore-1.46.0.dist-info → wbcore-1.58.2.dist-info}/RECORD +317 -271
- wbcore/contrib/geography/tests/test_serializers.py +0 -7
- wbcore/contrib/geography/tests/tests.py +0 -13
- wbcore/contrib/io/tests/tests.py +0 -19
- wbcore/contrib/workflow/tests/tests.py +0 -25
- {wbcore-1.46.0.dist-info → wbcore-1.58.2.dist-info}/WHEEL +0 -0
wbcore/enums.py
CHANGED
|
@@ -12,56 +12,6 @@ class RequestType(Enum):
|
|
|
12
12
|
HEAD = "head"
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
class Button(Enum):
|
|
16
|
-
# Buttons
|
|
17
|
-
REFRESH = "refresh"
|
|
18
|
-
NEW = "new"
|
|
19
|
-
DELETE = "delete"
|
|
20
|
-
|
|
21
|
-
# Buttons and Create Buttons
|
|
22
|
-
SAVE = "save"
|
|
23
|
-
SAVE_AND_CLOSE = "saveandclose"
|
|
24
|
-
SAVE_AND_NEW = "saveandnew"
|
|
25
|
-
|
|
26
|
-
# Create Buttons
|
|
27
|
-
RESET = "reset"
|
|
28
|
-
|
|
29
|
-
# Custom Buttons
|
|
30
|
-
DROPDOWN = "dropdown"
|
|
31
|
-
HYPERLINK = "hyperlink"
|
|
32
|
-
WIDGET = "widget"
|
|
33
|
-
ACTION = "action"
|
|
34
|
-
|
|
35
|
-
@classmethod
|
|
36
|
-
def buttons(cls):
|
|
37
|
-
return [
|
|
38
|
-
cls.REFRESH.value,
|
|
39
|
-
cls.NEW.value,
|
|
40
|
-
cls.DELETE.value,
|
|
41
|
-
cls.SAVE.value,
|
|
42
|
-
cls.SAVE_AND_CLOSE.value,
|
|
43
|
-
cls.SAVE_AND_NEW.value,
|
|
44
|
-
]
|
|
45
|
-
|
|
46
|
-
@classmethod
|
|
47
|
-
def create_buttons(cls):
|
|
48
|
-
return [
|
|
49
|
-
cls.SAVE.value,
|
|
50
|
-
cls.SAVE_AND_CLOSE.value,
|
|
51
|
-
cls.SAVE_AND_NEW.value,
|
|
52
|
-
cls.RESET.value,
|
|
53
|
-
]
|
|
54
|
-
|
|
55
|
-
@classmethod
|
|
56
|
-
def custom_buttons(cls):
|
|
57
|
-
return [
|
|
58
|
-
cls.DROPDOWN.value,
|
|
59
|
-
cls.HYPERLINK.value,
|
|
60
|
-
cls.WIDGET.value,
|
|
61
|
-
cls.ACTION.value,
|
|
62
|
-
]
|
|
63
|
-
|
|
64
|
-
|
|
65
15
|
class WidgetType(Enum):
|
|
66
16
|
LIST = "list"
|
|
67
17
|
INSTANCE = "instance"
|
|
@@ -89,7 +39,8 @@ class Unit(Enum):
|
|
|
89
39
|
return (float(_value), self.value)
|
|
90
40
|
|
|
91
41
|
def unit(self, _value: Union[float, str, int]):
|
|
92
|
-
|
|
42
|
+
if not isinstance(_value, (float, str, int)):
|
|
43
|
+
raise AssertionError("_value needs to be one of str, float or int")
|
|
93
44
|
|
|
94
45
|
return f"{float(_value)}{self.value}"
|
|
95
46
|
|
wbcore/filters/fields/choices.py
CHANGED
|
@@ -47,16 +47,14 @@ class MultipleChoiceFilter(WBCoreChoiceFilterMixin, django_filters.MultipleChoic
|
|
|
47
47
|
field_class = WBCoreMultipleChoiceField
|
|
48
48
|
filter_type = "select"
|
|
49
49
|
|
|
50
|
-
def
|
|
51
|
-
if
|
|
52
|
-
return
|
|
53
|
-
return
|
|
50
|
+
def _validate_initial_with_request(self, initial, request, name):
|
|
51
|
+
if request_initial := request.GET.get(name):
|
|
52
|
+
return request_initial.split(",")
|
|
53
|
+
return initial
|
|
54
54
|
|
|
55
55
|
def get_representation(self, request, name, view):
|
|
56
56
|
representation, lookup_expr = super().get_representation(request, name, view)
|
|
57
57
|
lookup_expr["input_properties"]["multiple"] = True
|
|
58
|
-
if (default := lookup_expr["input_properties"].get("default")) and not isinstance(default, list):
|
|
59
|
-
lookup_expr["input_properties"]["default"] = [default]
|
|
60
58
|
if (initial := lookup_expr["input_properties"].get("initial")) and not isinstance(initial, list):
|
|
61
59
|
lookup_expr["input_properties"]["initial"] = [initial]
|
|
62
60
|
return representation, lookup_expr
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import operator
|
|
2
3
|
from functools import reduce
|
|
3
4
|
|
|
@@ -14,6 +15,12 @@ class MultipleChoiceContentTypeFilter(WBCoreFilterMixin, django_filters.Filter):
|
|
|
14
15
|
field_class = ContentTypeMultiValueField
|
|
15
16
|
filter_type = "text"
|
|
16
17
|
|
|
18
|
+
def _validate_initial_with_request(self, initial, request, name):
|
|
19
|
+
initial = super()._validate_initial_with_request(initial, request, name)
|
|
20
|
+
if initial:
|
|
21
|
+
initial = json.dumps(initial)
|
|
22
|
+
return initial
|
|
23
|
+
|
|
17
24
|
def __init__(self, object_id_label="object_id", content_type_label="content_type", **kwargs):
|
|
18
25
|
self.object_id_label = object_id_label
|
|
19
26
|
self.content_type_label = content_type_label
|
|
@@ -22,13 +29,17 @@ class MultipleChoiceContentTypeFilter(WBCoreFilterMixin, django_filters.Filter):
|
|
|
22
29
|
def filter(self, qs, value):
|
|
23
30
|
if value in EMPTY_VALUES:
|
|
24
31
|
return qs
|
|
25
|
-
if self.distinct:
|
|
26
|
-
qs = qs.distinct()
|
|
27
32
|
conditions = [
|
|
28
33
|
(
|
|
29
34
|
Q(**{self.content_type_label: ContentType.objects.get_for_model(val)})
|
|
30
35
|
& Q(**{self.object_id_label: val.id})
|
|
31
36
|
)
|
|
32
|
-
for val in value
|
|
37
|
+
for val in filter(None, value)
|
|
33
38
|
]
|
|
34
|
-
|
|
39
|
+
if conditions:
|
|
40
|
+
qs = qs.filter(reduce(operator.or_, conditions))
|
|
41
|
+
else:
|
|
42
|
+
qs = qs.none()
|
|
43
|
+
if self.distinct:
|
|
44
|
+
qs = qs.distinct()
|
|
45
|
+
return qs
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
from contextlib import suppress
|
|
2
2
|
|
|
3
3
|
import django_filters
|
|
4
|
+
from django.contrib.postgres.fields import RangeField
|
|
5
|
+
from django_filters.constants import EMPTY_VALUES
|
|
6
|
+
from django_filters.utils import get_model_field
|
|
4
7
|
|
|
5
8
|
from wbcore.filters.mixins import WBCoreFilterMixin
|
|
6
9
|
from wbcore.forms import DateRangeField, DateTimeRangeField
|
|
@@ -28,10 +31,10 @@ class ShortcutAndPerformanceMixin(WBCoreFilterMixin):
|
|
|
28
31
|
|
|
29
32
|
def get_representation(self, request, name, view):
|
|
30
33
|
representation, lookup_expr = super().get_representation(request, name, view)
|
|
31
|
-
|
|
34
|
+
lookup_expr["input_properties"]["performance_mode"] = self.performance_mode
|
|
32
35
|
|
|
33
36
|
if self.shortcuts:
|
|
34
|
-
|
|
37
|
+
lookup_expr["input_properties"]["shortcuts"] = self.shortcuts
|
|
35
38
|
|
|
36
39
|
return representation, lookup_expr
|
|
37
40
|
|
|
@@ -39,54 +42,76 @@ class ShortcutAndPerformanceMixin(WBCoreFilterMixin):
|
|
|
39
42
|
class DateRangeFilter(ShortcutAndPerformanceMixin, django_filters.Filter):
|
|
40
43
|
field_class = DateRangeField
|
|
41
44
|
filter_type = "daterange"
|
|
42
|
-
|
|
45
|
+
initial_format = "%Y-%m-%d"
|
|
43
46
|
|
|
44
47
|
def __init__(self, *args, **kwargs):
|
|
45
48
|
kwargs.setdefault("lookup_expr", "overlap")
|
|
46
49
|
super().__init__(*args, **kwargs)
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if
|
|
51
|
+
@property
|
|
52
|
+
def is_range(self) -> bool:
|
|
53
|
+
if hasattr(self, "model"):
|
|
54
|
+
field = get_model_field(self.model, self.field_name)
|
|
55
|
+
return issubclass(field.__class__, RangeField)
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
def _get_initial(self, *args):
|
|
59
|
+
initial = super()._get_initial(*args)
|
|
60
|
+
if initial is not None:
|
|
51
61
|
lower = upper = None
|
|
52
|
-
if isinstance(
|
|
53
|
-
lower, upper =
|
|
62
|
+
if isinstance(initial, tuple):
|
|
63
|
+
lower, upper = initial
|
|
54
64
|
|
|
55
|
-
# if the
|
|
65
|
+
# if the initial is a tuple of components, we need to convert them to string
|
|
56
66
|
if isinstance(lower, Component) and isinstance(upper, Component):
|
|
57
67
|
return f"{lower},{upper}"
|
|
58
68
|
|
|
59
|
-
elif hasattr(
|
|
60
|
-
lower, upper =
|
|
61
|
-
|
|
62
|
-
return
|
|
69
|
+
elif hasattr(initial, "lower") and hasattr(initial, "upper"):
|
|
70
|
+
lower, upper = initial.lower, initial.upper
|
|
71
|
+
initial = f'{lower.strftime(self.initial_format) if lower else ""},{upper.strftime(self.initial_format) if upper else ""}'
|
|
72
|
+
return initial
|
|
63
73
|
|
|
64
74
|
def get_representation(self, request, name, view):
|
|
65
75
|
representation, lookup_expr = super().get_representation(request, name, view)
|
|
66
76
|
representation["lookup_expr"] = {"exact": self.field_name}
|
|
67
77
|
with suppress(KeyError): # TODO frontend needs to support both exact and overlaps lookup
|
|
68
|
-
|
|
69
|
-
|
|
78
|
+
initial = representation["initial"].pop(self.lookup_expr)
|
|
79
|
+
lookup_expr["input_properties"]["initial"]["exact"] = initial
|
|
70
80
|
|
|
71
81
|
return representation, lookup_expr
|
|
72
82
|
|
|
73
|
-
|
|
74
|
-
|
|
83
|
+
def filter(self, qs, value):
|
|
84
|
+
if value in EMPTY_VALUES:
|
|
85
|
+
return qs
|
|
75
86
|
if value:
|
|
87
|
+
lower, upper = value.lower, value.upper
|
|
76
88
|
filters = {}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
89
|
+
is_field_range = self.is_range
|
|
90
|
+
if lower:
|
|
91
|
+
if is_field_range:
|
|
92
|
+
filters[f"{self.field_name}__startswith__gte"] = lower
|
|
93
|
+
else:
|
|
94
|
+
filters[f"{self.field_name}__gte"] = lower
|
|
95
|
+
|
|
96
|
+
if upper:
|
|
97
|
+
if is_field_range:
|
|
98
|
+
filters[f"{self.field_name}__endswith__lte"] = upper
|
|
99
|
+
else:
|
|
100
|
+
filters[f"{self.field_name}__lte"] = upper
|
|
101
|
+
|
|
102
|
+
if self.exclude:
|
|
103
|
+
qs = qs.exclude(**filters)
|
|
104
|
+
else:
|
|
105
|
+
qs = qs.filter(**filters)
|
|
106
|
+
return qs
|
|
83
107
|
|
|
84
108
|
|
|
85
109
|
class FinancialPerformanceDateRangeFilter(DateRangeFilter):
|
|
86
110
|
def __init__(self, *args, **kwargs):
|
|
87
|
-
super().__init__(performance_mode=True, shortcuts=financial_performance_shortcuts,
|
|
111
|
+
super().__init__(*args, performance_mode=True, shortcuts=financial_performance_shortcuts, **kwargs)
|
|
88
112
|
|
|
89
113
|
|
|
90
114
|
class DateTimeRangeFilter(DateRangeFilter):
|
|
91
115
|
field_class = DateTimeRangeField
|
|
92
|
-
|
|
116
|
+
initial_format = "%Y-%m-%dT%H:%M:%S%z"
|
|
117
|
+
filter_type = "datetimerange"
|
wbcore/filters/fields/models.py
CHANGED
|
@@ -14,6 +14,11 @@ from wbcore.filters.mixins import WBCoreFilterMixin
|
|
|
14
14
|
class ModelChoiceFilterMixin(WBCoreFilterMixin):
|
|
15
15
|
MULTIPLE: bool = False
|
|
16
16
|
|
|
17
|
+
def _validate_initial_with_request(self, initial, request, name):
|
|
18
|
+
if request_default := request.GET.get(name):
|
|
19
|
+
return request_default.split(",")
|
|
20
|
+
return initial
|
|
21
|
+
|
|
17
22
|
@classmethod
|
|
18
23
|
def get_parsed_values(cls, queryset, value_ids: int | str | Iterable[int]):
|
|
19
24
|
if isinstance(value_ids, str):
|
|
@@ -28,8 +33,8 @@ class ModelChoiceFilterMixin(WBCoreFilterMixin):
|
|
|
28
33
|
"value": value_id,
|
|
29
34
|
"label": str(queryset.get(id=value_id)),
|
|
30
35
|
}
|
|
31
|
-
except ObjectDoesNotExist:
|
|
32
|
-
raise ParseError("Filter value invalid")
|
|
36
|
+
except ObjectDoesNotExist as e:
|
|
37
|
+
raise ParseError("Filter value invalid") from e
|
|
33
38
|
|
|
34
39
|
def get_representation(self, request, name, view):
|
|
35
40
|
representation, lookup_expr = super().get_representation(request, name, view)
|
|
@@ -45,19 +50,21 @@ class ModelChoiceFilterMixin(WBCoreFilterMixin):
|
|
|
45
50
|
url = reverse(self.endpoint, request=request)
|
|
46
51
|
if self.filter_params:
|
|
47
52
|
if callable(self.filter_params):
|
|
48
|
-
|
|
53
|
+
filter_params = self.filter_params(request, view)
|
|
49
54
|
else:
|
|
50
|
-
|
|
55
|
+
filter_params = self.filter_params
|
|
56
|
+
# we need to convert any list into comma seperated string
|
|
57
|
+
for key, value in filter_params.items():
|
|
58
|
+
if isinstance(value, list):
|
|
59
|
+
filter_params[key] = ",".join(map(lambda x: str(x), value))
|
|
60
|
+
|
|
61
|
+
url += f"?{urlencode(filter_params, doseq=True)}"
|
|
51
62
|
lookup_expr["input_properties"]["endpoint"] = {
|
|
52
63
|
"url": url,
|
|
53
64
|
"value_key": self.value_key,
|
|
54
65
|
"label_key": label_key,
|
|
55
66
|
}
|
|
56
67
|
|
|
57
|
-
if default_ids := lookup_expr["input_properties"].get("default", None):
|
|
58
|
-
values = self.get_parsed_values(queryset, default_ids)
|
|
59
|
-
lookup_expr["input_properties"]["default"] = list(values) if self.MULTIPLE else next(values)
|
|
60
|
-
|
|
61
68
|
if initial_ids := lookup_expr["input_properties"].get("initial", None):
|
|
62
69
|
values = self.get_parsed_values(queryset, initial_ids)
|
|
63
70
|
lookup_expr["input_properties"]["initial"] = list(values) if self.MULTIPLE else next(values)
|
|
@@ -86,12 +93,14 @@ class ModelMultipleChoiceFilter(ModelChoiceFilterMixin, django_filters.ModelMult
|
|
|
86
93
|
self.filter_params = kwargs.pop("filter_params", None)
|
|
87
94
|
self.value_key = kwargs.pop("value_key", None)
|
|
88
95
|
self.label_key = kwargs.pop("label_key", None)
|
|
89
|
-
|
|
90
96
|
# TODO: This is monkeypatched. Make sure that the CSVWidget is set here and only here!
|
|
91
97
|
if "widget" not in kwargs:
|
|
92
98
|
kwargs["widget"] = django_filters.widgets.CSVWidget
|
|
93
99
|
super().__init__(*args, **kwargs)
|
|
94
100
|
|
|
101
|
+
# django filter sets it to True by default. In our case, the fitlering will happen on primary keys, so we do not expect any duplicate. Furthermore, for table without explicit unique constraint, using "distinct" leads to unexpected results (i.e. row with same value are dropped)
|
|
102
|
+
self.distinct = False
|
|
103
|
+
|
|
95
104
|
|
|
96
105
|
class ModelChoiceFilter(ModelChoiceFilterMixin, django_filters.ModelChoiceFilter):
|
|
97
106
|
class SimpleModelChoiceField(ModelChoiceField):
|
wbcore/filters/fields/numbers.py
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
import django_filters
|
|
2
2
|
|
|
3
3
|
from wbcore.filters.mixins import WBCoreFilterMixin
|
|
4
|
+
from wbcore.serializers import WBCoreType
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
class NumberFilter(WBCoreFilterMixin, django_filters.NumberFilter):
|
|
7
8
|
filter_type = "number"
|
|
8
9
|
|
|
9
|
-
def __init__(self, precision: int = 0, percent: bool = False,
|
|
10
|
+
def __init__(self, precision: int = 0, percent: bool = False, disable_formatting: bool = False, *args, **kwargs):
|
|
10
11
|
self.precision = precision
|
|
11
12
|
self.percent = percent
|
|
12
|
-
self.
|
|
13
|
-
self.decimal_mark = decimal_mark
|
|
13
|
+
self.disable_formatting = disable_formatting
|
|
14
14
|
super().__init__(*args, **kwargs)
|
|
15
15
|
|
|
16
16
|
def get_representation(self, request, name, view):
|
|
17
17
|
representation, lookup_expr = super().get_representation(request, name, view)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
lookup_expr["input_properties"]["precision"] = self.precision
|
|
19
|
+
if self.percent: # TODO: Discuss with Christoph if this is necessary like this
|
|
20
|
+
lookup_expr["input_properties"]["type"] = WBCoreType.PERCENT.value
|
|
21
|
+
lookup_expr["input_properties"]["precision"] = max(self.precision - 2, 0)
|
|
22
|
+
lookup_expr["input_properties"]["disable_formatting"] = self.disable_formatting
|
|
21
23
|
return representation, lookup_expr
|
|
22
24
|
|
|
23
25
|
def filter(self, qs, value):
|
|
@@ -29,8 +31,7 @@ class NumberFilter(WBCoreFilterMixin, django_filters.NumberFilter):
|
|
|
29
31
|
class YearFilter(NumberFilter):
|
|
30
32
|
def __init__(self, *args, **kwargs):
|
|
31
33
|
super().__init__(*args, **kwargs)
|
|
32
|
-
self.
|
|
33
|
-
self.decimal_mark = "."
|
|
34
|
+
self.disable_formatting = True
|
|
34
35
|
|
|
35
36
|
|
|
36
37
|
class RangeSelectFilter(NumberFilter):
|
wbcore/filters/filterset.py
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from collections import OrderedDict
|
|
2
3
|
from contextlib import suppress
|
|
4
|
+
from copy import copy
|
|
3
5
|
|
|
4
6
|
from django.contrib.postgres.fields import DateRangeField, DateTimeRangeField
|
|
5
7
|
from django.core.exceptions import FieldError
|
|
6
8
|
from django.db import models
|
|
9
|
+
from django.db.models import GeneratedField
|
|
7
10
|
from django.db.models.fields.related import ManyToManyRel, ManyToOneRel, OneToOneRel
|
|
8
11
|
from django_filters.filterset import FilterSetMetaclass, remote_queryset, settings
|
|
9
12
|
from django_filters.rest_framework import FilterSet as DjangoFilterSet
|
|
10
13
|
|
|
11
14
|
from wbcore.filters import fields
|
|
12
15
|
from wbcore.filters.fields.multiple_lookups import MultipleLookupFilter
|
|
16
|
+
from wbcore.models.fields import YearField
|
|
13
17
|
from wbcore.signals.filters import add_filters
|
|
14
18
|
|
|
15
19
|
from .utils import check_required_filters
|
|
@@ -26,8 +30,8 @@ def _is_number(field):
|
|
|
26
30
|
|
|
27
31
|
|
|
28
32
|
class CustomFilterSetMetaClass(FilterSetMetaclass):
|
|
29
|
-
def __new__(cls,
|
|
30
|
-
new_class = super().__new__(cls,
|
|
33
|
+
def __new__(cls, *args, **kwargs):
|
|
34
|
+
new_class = super().__new__(cls, *args, **kwargs)
|
|
31
35
|
if _meta := getattr(new_class, "Meta", None):
|
|
32
36
|
for parent_field_name, child_fields in getattr(_meta, "flatten_fields", dict()).items():
|
|
33
37
|
if remote_field := getattr(_meta.model._meta.get_field(parent_field_name), "remote_field", None):
|
|
@@ -63,6 +67,7 @@ class CustomFilterSetMetaClass(FilterSetMetaclass):
|
|
|
63
67
|
|
|
64
68
|
|
|
65
69
|
class FilterSet(DjangoFilterSet, metaclass=CustomFilterSetMetaClass):
|
|
70
|
+
DEFAULT_EXCLUDE_FILTER_LOOKUP: str = "exclude"
|
|
66
71
|
FILTER_DEFAULTS = {
|
|
67
72
|
models.BooleanField: {"filter_class": fields.BooleanFilter},
|
|
68
73
|
models.NullBooleanField: {"filter_class": fields.BooleanFilter},
|
|
@@ -83,6 +88,7 @@ class FilterSet(DjangoFilterSet, metaclass=CustomFilterSetMetaClass):
|
|
|
83
88
|
models.DecimalField: {"filter_class": fields.NumberFilter},
|
|
84
89
|
DateTimeRangeField: {"filter_class": fields.DateTimeRangeFilter},
|
|
85
90
|
DateRangeField: {"filter_class": fields.DateRangeFilter},
|
|
91
|
+
YearField: {"filter_class": fields.YearFilter},
|
|
86
92
|
# models.DurationField: {"filter_class": DurationFilter},
|
|
87
93
|
# models.SmallIntegerField: {"filter_class": NumberFilter},
|
|
88
94
|
# models.AutoField: {"filter_class": NumberFilter},
|
|
@@ -165,11 +171,13 @@ class FilterSet(DjangoFilterSet, metaclass=CustomFilterSetMetaClass):
|
|
|
165
171
|
for _, res in remote_filters:
|
|
166
172
|
if res:
|
|
167
173
|
for remote_filter_key, remote_filter in res.items():
|
|
168
|
-
|
|
174
|
+
remote_filter.column_field_name = remote_filter_key
|
|
169
175
|
self.filters[remote_filter_key] = remote_filter
|
|
170
176
|
|
|
171
177
|
@classmethod
|
|
172
178
|
def filter_for_lookup(cls, field, lookup_type):
|
|
179
|
+
if isinstance(field, GeneratedField):
|
|
180
|
+
return cls.filter_for_lookup(field.output_field, lookup_type)
|
|
173
181
|
if lookup_type == "exact" and getattr(field, "choices", None):
|
|
174
182
|
filter_class, params = fields.ChoiceFilter, {"choices": field.choices}
|
|
175
183
|
else:
|
|
@@ -191,12 +199,12 @@ class FilterSet(DjangoFilterSet, metaclass=CustomFilterSetMetaClass):
|
|
|
191
199
|
|
|
192
200
|
@classmethod
|
|
193
201
|
def get_filters(cls):
|
|
194
|
-
filters = super().get_filters()
|
|
202
|
+
filters = dict(super().get_filters())
|
|
195
203
|
remote_filters = add_filters.send(sender=cls.filter_class_for_remote_filter())
|
|
196
204
|
for _, res in remote_filters:
|
|
197
205
|
if res:
|
|
198
206
|
for remote_filter_key, remote_filter in res.items():
|
|
199
|
-
|
|
207
|
+
remote_filter.column_field_name = remote_filter_key
|
|
200
208
|
filters[remote_filter_key] = remote_filter
|
|
201
209
|
|
|
202
210
|
for field, help_text in getattr(cls, "help_texts", {}).items():
|
|
@@ -206,7 +214,20 @@ class FilterSet(DjangoFilterSet, metaclass=CustomFilterSetMetaClass):
|
|
|
206
214
|
for field, values in cls.get_dependency_map():
|
|
207
215
|
for value in values:
|
|
208
216
|
filters[field].depends_on.append({"field": value, "options": {}})
|
|
209
|
-
|
|
217
|
+
|
|
218
|
+
excluding_fields = {}
|
|
219
|
+
for name, field in filters.items():
|
|
220
|
+
# if allow_exclude is true, we add a copy of the field with the parameter exclude=True
|
|
221
|
+
# (to use `exclude` queryset method instead of `filter`) and add this with the suffix __{cls.DEFAULT_EXCLUDE_FILTER_LOOKUP}
|
|
222
|
+
if field.allow_exclude:
|
|
223
|
+
excluding_field = copy(field)
|
|
224
|
+
excluding_field.exclude = True
|
|
225
|
+
excluding_field.excluded_filter = True
|
|
226
|
+
excluding_field.hidden = True
|
|
227
|
+
excluding_field.required = False
|
|
228
|
+
excluding_fields[f"{name}__{cls.DEFAULT_EXCLUDE_FILTER_LOOKUP}"] = excluding_field
|
|
229
|
+
filters.update(excluding_fields)
|
|
230
|
+
return OrderedDict(filters)
|
|
210
231
|
|
|
211
232
|
def extract_required_field_labels(self):
|
|
212
233
|
return [label for label, filter in self.base_filters.items() if getattr(filter, "required", False)]
|
wbcore/filters/mixins.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import warnings
|
|
1
2
|
from contextlib import suppress
|
|
2
3
|
|
|
4
|
+
from django.core.exceptions import ValidationError
|
|
3
5
|
from django_filters.utils import get_model_field
|
|
4
6
|
|
|
5
7
|
from .lookups import get_lookup_icon, get_lookup_label
|
|
@@ -7,18 +9,31 @@ from .lookups import get_lookup_icon, get_lookup_label
|
|
|
7
9
|
|
|
8
10
|
class WBCoreFilterMixin:
|
|
9
11
|
def __init__(self, *args, **kwargs):
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
default = kwargs.pop("default", None)
|
|
13
|
+
if default is not None:
|
|
14
|
+
self.initial = default
|
|
15
|
+
self.required = True
|
|
16
|
+
warnings.warn(
|
|
17
|
+
"The use of default will be deprecated in favour of the equivalent 'initial' and 'required=True'",
|
|
18
|
+
DeprecationWarning,
|
|
19
|
+
stacklevel=2,
|
|
20
|
+
)
|
|
21
|
+
else:
|
|
22
|
+
self.initial = kwargs.pop("initial", None)
|
|
23
|
+
self.required = kwargs.pop("required", False)
|
|
13
24
|
self.clearable = kwargs.pop("clearable", True) # TODO: Take away
|
|
14
25
|
self.hidden = kwargs.pop("hidden", False)
|
|
15
26
|
self.column_field_name = kwargs.pop("column_field_name", None)
|
|
16
27
|
self.help_text = kwargs.pop("help_text", None)
|
|
17
|
-
self.
|
|
28
|
+
self.allow_empty_initial = kwargs.pop("allow_empty_initial", False)
|
|
18
29
|
self.label_format = kwargs.pop(
|
|
19
30
|
"label_format",
|
|
20
31
|
getattr(self, "default_label_format", "{{field_label}} {{operation_icon}} {{value_label}}"),
|
|
21
32
|
)
|
|
33
|
+
self.allow_exclude = kwargs.pop(
|
|
34
|
+
"allow_exclude", kwargs.get("method") is None
|
|
35
|
+
) # if False, we will not automatically add a similar filter "opposite" filter
|
|
36
|
+
self.excluded_filter = kwargs.pop("excluded_filter", False)
|
|
22
37
|
self.lookup_icon = kwargs.pop("lookup_icon", None)
|
|
23
38
|
self.lookup_label = kwargs.pop("lookup_label", None)
|
|
24
39
|
self.depends_on = kwargs.pop("depends_on", [])
|
|
@@ -34,25 +49,28 @@ class WBCoreFilterMixin:
|
|
|
34
49
|
else:
|
|
35
50
|
return self.label
|
|
36
51
|
|
|
37
|
-
def
|
|
38
|
-
# We consider the case where
|
|
39
|
-
if callable(self.
|
|
40
|
-
|
|
52
|
+
def _get_initial(self, request, view):
|
|
53
|
+
# We consider the case where initial is a boolean with value False.
|
|
54
|
+
if callable(self.initial):
|
|
55
|
+
initial = self.initial(self, request, view)
|
|
41
56
|
elif (
|
|
42
|
-
isinstance(self.
|
|
43
|
-
and (
|
|
44
|
-
and (callable(
|
|
57
|
+
isinstance(self.initial, str)
|
|
58
|
+
and (callable_initial := getattr(self, self.initial, None))
|
|
59
|
+
and (callable(callable_initial))
|
|
45
60
|
):
|
|
46
|
-
|
|
61
|
+
initial = callable_initial(self, request, view)
|
|
47
62
|
else:
|
|
48
|
-
|
|
63
|
+
initial = self.initial
|
|
49
64
|
|
|
50
|
-
return
|
|
65
|
+
return initial
|
|
51
66
|
|
|
52
|
-
def
|
|
67
|
+
def _validate_initial_with_request(self, initial, request, name):
|
|
53
68
|
if request_default := request.GET.get(name):
|
|
54
|
-
|
|
55
|
-
|
|
69
|
+
try:
|
|
70
|
+
return self.field.to_python(request_default)
|
|
71
|
+
except ValidationError:
|
|
72
|
+
return None
|
|
73
|
+
return initial
|
|
56
74
|
|
|
57
75
|
def get_help_text(self) -> str:
|
|
58
76
|
if self.help_text:
|
|
@@ -65,9 +83,6 @@ class WBCoreFilterMixin:
|
|
|
65
83
|
return "Filter by " + self.label
|
|
66
84
|
|
|
67
85
|
def get_representation(self, request, name, view):
|
|
68
|
-
if self.hidden:
|
|
69
|
-
return {}
|
|
70
|
-
|
|
71
86
|
representation = {
|
|
72
87
|
"key": self.key,
|
|
73
88
|
"label_format": self.label_format,
|
|
@@ -79,34 +94,18 @@ class WBCoreFilterMixin:
|
|
|
79
94
|
"icon": get_lookup_icon(self.lookup_expr) if self.lookup_icon is None else self.lookup_icon,
|
|
80
95
|
"key": name,
|
|
81
96
|
"hidden": self.hidden,
|
|
97
|
+
"allow_exclude": self.allow_exclude,
|
|
82
98
|
"input_properties": {
|
|
83
99
|
"type": self.filter_type,
|
|
84
100
|
},
|
|
85
101
|
}
|
|
86
|
-
|
|
87
|
-
if
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
initial = None
|
|
91
|
-
if callable(self.initial):
|
|
92
|
-
initial = self.initial(self, request, view)
|
|
93
|
-
elif (
|
|
94
|
-
isinstance(self.initial, str)
|
|
95
|
-
and (callable_initial := getattr(self, self.initial, None))
|
|
96
|
-
and (callable(callable_initial))
|
|
97
|
-
):
|
|
98
|
-
initial = callable_initial(self, request, view)
|
|
99
|
-
else:
|
|
100
|
-
initial = self.initial
|
|
101
|
-
|
|
102
|
-
if _initial := self._validate_default_with_request(initial, request, name):
|
|
103
|
-
initial = _initial
|
|
102
|
+
initial = self._get_initial(request, view)
|
|
103
|
+
if (overridden_initial := self._validate_initial_with_request(initial, request, name)) is not None:
|
|
104
|
+
initial = overridden_initial
|
|
104
105
|
|
|
105
|
-
if initial is not None:
|
|
106
|
+
if initial is not None or self.allow_empty_initial:
|
|
106
107
|
lookup_expr["input_properties"]["initial"] = initial
|
|
107
108
|
|
|
108
|
-
|
|
109
|
-
lookup_expr["input_properties"]["required"] = True
|
|
110
|
-
# assert representation["default"] != {}, "If a filter is required, it needs at least one default value"
|
|
109
|
+
lookup_expr["input_properties"]["required"] = self.required
|
|
111
110
|
representation["depends_on"] = self.depends_on
|
|
112
111
|
return representation, lookup_expr
|
wbcore/forms.py
CHANGED
|
@@ -3,7 +3,7 @@ from urllib.parse import unquote
|
|
|
3
3
|
|
|
4
4
|
from django import forms
|
|
5
5
|
from django.contrib.contenttypes.models import ContentType
|
|
6
|
-
from django.core.exceptions import ValidationError
|
|
6
|
+
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|
7
7
|
from django.forms import modelformset_factory
|
|
8
8
|
from django.forms.models import BaseModelFormSet
|
|
9
9
|
from psycopg.types.range import DateRange, TimestamptzRange
|
|
@@ -39,10 +39,10 @@ def nonrelated_inlineformset_factory(
|
|
|
39
39
|
"""
|
|
40
40
|
FormSet factory that sets an explicit queryset on new classes.
|
|
41
41
|
"""
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return
|
|
42
|
+
form = modelformset_factory(model, formset=formset, **kwargs)
|
|
43
|
+
form.real_queryset = queryset
|
|
44
|
+
form.save_new_instance = save_new_instance
|
|
45
|
+
return form
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
class ContentTypeMultiValueField(forms.fields.MultiValueField):
|
|
@@ -68,7 +68,7 @@ class ContentTypeMultiValueField(forms.fields.MultiValueField):
|
|
|
68
68
|
try:
|
|
69
69
|
content_type = ContentType.objects.get_for_id(content_type_id)
|
|
70
70
|
return content_type.get_object_for_this_type(id=object_id)
|
|
71
|
-
except
|
|
71
|
+
except ObjectDoesNotExist:
|
|
72
72
|
return None
|
|
73
73
|
|
|
74
74
|
|