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
|
@@ -1,6 +1,100 @@
|
|
|
1
1
|
import pytest
|
|
2
|
+
from rest_framework import status
|
|
3
|
+
|
|
4
|
+
from wbcore.contrib.geography.factories import ContinentFactory
|
|
5
|
+
from wbcore.contrib.geography.models import Geography
|
|
6
|
+
from wbcore.contrib.geography.serializers import GeographyModelSerializer
|
|
7
|
+
from wbcore.contrib.geography.viewsets import (
|
|
8
|
+
GeographyModelViewSet,
|
|
9
|
+
GeographyRepresentationViewSet,
|
|
10
|
+
)
|
|
2
11
|
|
|
3
12
|
|
|
4
13
|
@pytest.mark.django_db
|
|
5
|
-
|
|
6
|
-
|
|
14
|
+
@pytest.mark.with_db
|
|
15
|
+
class TestViewsets:
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def continents(self):
|
|
18
|
+
return ContinentFactory.create_batch(3)
|
|
19
|
+
|
|
20
|
+
@pytest.fixture
|
|
21
|
+
def continent(self, continents):
|
|
22
|
+
return continents[0]
|
|
23
|
+
|
|
24
|
+
@pytest.fixture
|
|
25
|
+
def continent_build(self):
|
|
26
|
+
instance = ContinentFactory.build()
|
|
27
|
+
return GeographyModelSerializer(instance).data
|
|
28
|
+
|
|
29
|
+
@pytest.mark.parametrize("viewset", [GeographyModelViewSet, GeographyRepresentationViewSet])
|
|
30
|
+
def test_get(self, viewset, request_factory, super_user, continents):
|
|
31
|
+
# Arrange
|
|
32
|
+
request = request_factory.get("")
|
|
33
|
+
request.user = super_user
|
|
34
|
+
viewset = viewset.as_view({"get": "list"})
|
|
35
|
+
# Act
|
|
36
|
+
response = viewset(request)
|
|
37
|
+
# Assert
|
|
38
|
+
assert len(response.data["results"]) == 3
|
|
39
|
+
assert response.status_code == status.HTTP_200_OK
|
|
40
|
+
|
|
41
|
+
@pytest.mark.parametrize("viewset", [GeographyModelViewSet, GeographyRepresentationViewSet])
|
|
42
|
+
def test_retrieve(self, viewset, request_factory, super_user, continent):
|
|
43
|
+
# Arrange
|
|
44
|
+
request = request_factory.get("")
|
|
45
|
+
request.user = super_user
|
|
46
|
+
viewset = viewset.as_view({"get": "retrieve"})
|
|
47
|
+
# Act
|
|
48
|
+
response = viewset(request, pk=continent.id)
|
|
49
|
+
instance = response.data.get("instance")
|
|
50
|
+
# Assert
|
|
51
|
+
assert instance is not None
|
|
52
|
+
assert instance["id"] == continent.id
|
|
53
|
+
assert response.status_code == status.HTTP_200_OK
|
|
54
|
+
|
|
55
|
+
def test_create(self, request_factory, super_user, continent_build):
|
|
56
|
+
# Arrange
|
|
57
|
+
request = request_factory.post("", data=continent_build, format="json")
|
|
58
|
+
request.user = super_user
|
|
59
|
+
viewset = GeographyModelViewSet.as_view({"post": "create"})
|
|
60
|
+
# Act
|
|
61
|
+
response = viewset(request)
|
|
62
|
+
# Assert
|
|
63
|
+
assert response.status_code == status.HTTP_201_CREATED
|
|
64
|
+
|
|
65
|
+
def test_delete(self, request_factory, super_user, continents):
|
|
66
|
+
# Arrange
|
|
67
|
+
entry_id = continents[1].id
|
|
68
|
+
request = request_factory.delete("", args=entry_id)
|
|
69
|
+
request.user = super_user
|
|
70
|
+
viewset = GeographyModelViewSet.as_view({"delete": "destroy"})
|
|
71
|
+
# Act
|
|
72
|
+
response = viewset(request, pk=entry_id)
|
|
73
|
+
# Assert
|
|
74
|
+
assert response.status_code == status.HTTP_204_NO_CONTENT
|
|
75
|
+
assert Geography.objects.count() == 2
|
|
76
|
+
assert not Geography.objects.filter(id=entry_id).exists()
|
|
77
|
+
|
|
78
|
+
def test_put(self, request_factory, super_user, continent, continent_build):
|
|
79
|
+
# Arrange
|
|
80
|
+
continent_build["id"] = continent.id
|
|
81
|
+
request = request_factory.put("", data=continent_build, format="json")
|
|
82
|
+
request.user = super_user
|
|
83
|
+
viewset = GeographyModelViewSet.as_view({"put": "update"})
|
|
84
|
+
# Act
|
|
85
|
+
response = viewset(request, pk=continent.id)
|
|
86
|
+
# Assert
|
|
87
|
+
assert response.status_code == status.HTTP_200_OK
|
|
88
|
+
|
|
89
|
+
def test_patch(self, request_factory, super_user, continent):
|
|
90
|
+
# Arrange
|
|
91
|
+
new_field_data = "Foo Bar"
|
|
92
|
+
request = request_factory.patch("", data={"name": new_field_data})
|
|
93
|
+
request.user = super_user
|
|
94
|
+
viewset = GeographyModelViewSet.as_view({"patch": "partial_update"})
|
|
95
|
+
# Act
|
|
96
|
+
response = viewset(request, pk=continent.id)
|
|
97
|
+
continent.refresh_from_db()
|
|
98
|
+
# Assert
|
|
99
|
+
assert response.status_code == status.HTTP_200_OK
|
|
100
|
+
assert continent.name == new_field_data
|
|
@@ -4,8 +4,7 @@ from wbcore.contrib.guardian.models.mixins import PermissionObjectModelMixin
|
|
|
4
4
|
|
|
5
5
|
@pytest.fixture
|
|
6
6
|
def mocked_permission_object_model_mixin(mocker):
|
|
7
|
-
def __init__(self):
|
|
8
|
-
...
|
|
7
|
+
def __init__(self): ... # noqa
|
|
9
8
|
|
|
10
9
|
PermissionObjectModelMixin.__init__ = __init__
|
|
11
10
|
PermissionObjectModelMixin.objects = mocker.Mock()
|
|
@@ -22,8 +21,8 @@ class TestPermissionObjectModelMixin:
|
|
|
22
21
|
def test_save_run_assign_permissions(self, mocker, mocked_permission_object_model_mixin):
|
|
23
22
|
mocker.patch("django.db.models.Model.save")
|
|
24
23
|
on_commit = mocker.patch("wbcore.contrib.guardian.models.mixins.transaction.on_commit")
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
content_type_class = mocker.patch("wbcore.contrib.guardian.models.mixins.ContentType")
|
|
25
|
+
content_type_class.objects.get_for_model.return_value = mocker.Mock()
|
|
27
26
|
|
|
28
27
|
mocked_permission_object_model_mixin().save()
|
|
29
28
|
|
|
@@ -8,39 +8,39 @@ from wbcore.contrib.guardian.models.mixins import (
|
|
|
8
8
|
|
|
9
9
|
class TestAssignUserPermissionsForObjectAsTask:
|
|
10
10
|
def test_called(self, mocker):
|
|
11
|
-
|
|
11
|
+
content_type_class = mocker.patch("wbcore.contrib.guardian.models.mixins.ContentType")
|
|
12
12
|
content_type = mocker.Mock()
|
|
13
13
|
model_class = mocker.Mock()
|
|
14
14
|
instance = mocker.Mock()
|
|
15
15
|
model_class.objects.get.return_value = instance
|
|
16
16
|
content_type.model_class.return_value = model_class
|
|
17
|
-
|
|
17
|
+
content_type_class.objects.get.return_value = content_type
|
|
18
18
|
|
|
19
19
|
assign_user_permissions_for_object_as_task(1, 1)
|
|
20
20
|
|
|
21
21
|
instance.reload_permissions.assert_called_once_with(prune_existing=True)
|
|
22
22
|
|
|
23
23
|
def test_called_with_prune_existing(self, mocker):
|
|
24
|
-
|
|
24
|
+
content_type_class = mocker.patch("wbcore.contrib.guardian.models.mixins.ContentType")
|
|
25
25
|
content_type = mocker.Mock()
|
|
26
26
|
model_class = mocker.Mock()
|
|
27
27
|
instance = mocker.Mock()
|
|
28
28
|
model_class.objects.get.return_value = instance
|
|
29
29
|
content_type.model_class.return_value = model_class
|
|
30
|
-
|
|
30
|
+
content_type_class.objects.get.return_value = content_type
|
|
31
31
|
|
|
32
32
|
assign_user_permissions_for_object_as_task(1, 1, prune_existing=False)
|
|
33
33
|
|
|
34
34
|
instance.reload_permissions.assert_called_once_with(prune_existing=False)
|
|
35
35
|
|
|
36
36
|
def test_not_called_without_model_class(self, mocker):
|
|
37
|
-
|
|
37
|
+
content_type_class = mocker.patch("wbcore.contrib.guardian.models.mixins.ContentType")
|
|
38
38
|
content_type = mocker.Mock()
|
|
39
39
|
model_class = mocker.Mock()
|
|
40
40
|
instance = mocker.Mock()
|
|
41
41
|
model_class.objects.get.return_value = instance
|
|
42
42
|
content_type.model_class.return_value = None
|
|
43
|
-
|
|
43
|
+
content_type_class.objects.get.return_value = content_type
|
|
44
44
|
|
|
45
45
|
assign_user_permissions_for_object_as_task(1, 1)
|
|
46
46
|
|
|
@@ -63,13 +63,13 @@ class TestAssignUserPermissionsForObjectAsTask:
|
|
|
63
63
|
|
|
64
64
|
class TestAssignObjectPermissionsForUserAsTask:
|
|
65
65
|
def test_called(self, mocker):
|
|
66
|
-
|
|
66
|
+
user_class = mocker.patch("wbcore.contrib.guardian.models.mixins.User")
|
|
67
67
|
user = mocker.Mock()
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
user_class.objects.get.return_value = user
|
|
70
70
|
|
|
71
71
|
get_inheriting_subclasses = mocker.patch("wbcore.contrib.guardian.models.mixins.get_inheriting_subclasses")
|
|
72
|
-
get_inheriting_subclasses.return_value = [
|
|
72
|
+
get_inheriting_subclasses.return_value = [user_class]
|
|
73
73
|
reload_permissions = mocker.patch("wbcore.contrib.guardian.models.mixins.reload_permissions")
|
|
74
74
|
|
|
75
75
|
assign_object_permissions_for_user_as_task(user_id=1)
|
|
@@ -10,7 +10,7 @@ class TestPivotUserObjectPermissionModelViewSet:
|
|
|
10
10
|
def test_cached_property_permissions(self, mocker, request):
|
|
11
11
|
mocked_filter = mocker.patch("wbcore.contrib.guardian.viewsets.viewsets.Permission.objects.filter")
|
|
12
12
|
view = PivotUserObjectPermissionModelViewSet(request=request, kwargs={"content_type_id": 1})
|
|
13
|
-
view.permissions
|
|
13
|
+
assert view.permissions
|
|
14
14
|
|
|
15
15
|
mocked_filter.assert_called_once_with(
|
|
16
16
|
Q(content_type=1)
|
|
@@ -24,7 +24,7 @@ class TestPivotUserObjectPermissionModelViewSet:
|
|
|
24
24
|
mocked_get_object_for_this_type = mocker.Mock()
|
|
25
25
|
mocked_contrib_type.get.return_value = mocked_get_object_for_this_type
|
|
26
26
|
view = PivotUserObjectPermissionModelViewSet(request=request, kwargs={"content_type_id": 1, "object_pk": 1})
|
|
27
|
-
view.linked_object
|
|
27
|
+
assert view.linked_object
|
|
28
28
|
|
|
29
29
|
mocked_get_object_for_this_type.get_object_for_this_type.assert_called_once()
|
|
30
30
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from .buttons import add_object_permission_button
|
|
1
|
+
from .buttons import add_object_permission_button, PivotUserObjectPermissionButtonViewConfig
|
|
2
2
|
from .displays import PivotUserObjectPermissionDisplayViewConfig
|
|
3
3
|
from .endpoints import PivotUserObjectPermissionEndpointViewConfig
|
|
4
4
|
from .titles import PivotUserObjectPermissionTitleViewConfig
|
|
@@ -4,10 +4,19 @@ from django.dispatch import receiver
|
|
|
4
4
|
from rest_framework.reverse import reverse
|
|
5
5
|
from wbcore.contrib.guardian.models.mixins import PermissionObjectModelMixin
|
|
6
6
|
from wbcore.contrib.icons.icons import WBIcon
|
|
7
|
+
from wbcore.metadata.configs.buttons import ButtonViewConfig
|
|
7
8
|
from wbcore.metadata.configs.buttons.buttons import WidgetButton
|
|
9
|
+
from wbcore.metadata.configs.buttons.enums import Button
|
|
8
10
|
from wbcore.signals.instance_buttons import add_extra_button
|
|
9
11
|
|
|
10
12
|
|
|
13
|
+
class PivotUserObjectPermissionButtonViewConfig(ButtonViewConfig):
|
|
14
|
+
def get_create_buttons(self):
|
|
15
|
+
return {
|
|
16
|
+
Button.SAVE_AND_CLOSE.value,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
11
20
|
@receiver(add_extra_button)
|
|
12
21
|
def add_object_permission_button(sender, instance, request, view, pk=None, **kwargs):
|
|
13
22
|
with suppress(AttributeError):
|
|
@@ -3,6 +3,13 @@ from wbcore.metadata.configs.endpoints import EndpointViewConfig
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class PivotUserObjectPermissionEndpointViewConfig(EndpointViewConfig):
|
|
6
|
+
def get_instance_endpoint(self, **kwargs):
|
|
7
|
+
return reverse(
|
|
8
|
+
"wbcore:guardian:pivoteduserobjectpermission-list",
|
|
9
|
+
args=[self.view.kwargs.get("content_type_id", None), self.view.kwargs.get("object_pk", None)],
|
|
10
|
+
request=self.request,
|
|
11
|
+
)
|
|
12
|
+
|
|
6
13
|
def get_update_endpoint(self, **kwargs):
|
|
7
14
|
return reverse(
|
|
8
15
|
"wbcore:guardian:pivoteduserobjectpermission-list",
|
|
@@ -8,6 +8,7 @@ from wbcore.contrib.authentication.models.users import Permission, User
|
|
|
8
8
|
from wbcore.contrib.authentication.serializers import UserRepresentationSerializer
|
|
9
9
|
from wbcore.contrib.guardian.models import UserObjectPermission
|
|
10
10
|
from wbcore.contrib.guardian.viewsets.configs import (
|
|
11
|
+
PivotUserObjectPermissionButtonViewConfig,
|
|
11
12
|
PivotUserObjectPermissionDisplayViewConfig,
|
|
12
13
|
PivotUserObjectPermissionEndpointViewConfig,
|
|
13
14
|
PivotUserObjectPermissionTitleViewConfig,
|
|
@@ -17,6 +18,7 @@ from wbcore.contrib.guardian.viewsets.configs import (
|
|
|
17
18
|
class PivotUserObjectPermissionModelViewSet(viewsets.ModelViewSet):
|
|
18
19
|
queryset = UserObjectPermission.objects.all()
|
|
19
20
|
|
|
21
|
+
button_config_class = PivotUserObjectPermissionButtonViewConfig
|
|
20
22
|
display_config_class = PivotUserObjectPermissionDisplayViewConfig
|
|
21
23
|
endpoint_config_class = PivotUserObjectPermissionEndpointViewConfig
|
|
22
24
|
title_config_class = PivotUserObjectPermissionTitleViewConfig
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from django.dispatch import receiver
|
|
2
|
+
from django.urls import resolve
|
|
3
|
+
from rest_framework.request import Request
|
|
4
|
+
from rest_framework.reverse import reverse
|
|
5
|
+
|
|
6
|
+
from wbcore import serializers
|
|
7
|
+
from wbcore.contrib.icons.icons import WBIcon
|
|
8
|
+
from wbcore.enums import RequestType
|
|
9
|
+
from wbcore.metadata.configs.buttons import ActionButton
|
|
10
|
+
from wbcore.metadata.configs.buttons.enums import ButtonDefaultColor
|
|
11
|
+
from wbcore.metadata.configs.display.instance_display.shortcuts import create_simple_display
|
|
12
|
+
from wbcore.signals.instance_buttons import add_extra_button
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AutoTranslateSerializer(serializers.Serializer):
|
|
16
|
+
override_existing_data = serializers.BooleanField(default=False)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@receiver(add_extra_button)
|
|
20
|
+
def add_auto_translate_action_button(sender, instance, request: Request, view, pk=None, **kwargs):
|
|
21
|
+
if instance and pk and view and hasattr(view, "auto_translate"):
|
|
22
|
+
url = reverse(resolve(request.path).view_name.replace("detail", "auto-translate"), args=[pk], request=request)
|
|
23
|
+
return ActionButton(
|
|
24
|
+
method=RequestType.POST,
|
|
25
|
+
icon=WBIcon.DEAL.icon,
|
|
26
|
+
color=ButtonDefaultColor.SUCCESS,
|
|
27
|
+
endpoint=url,
|
|
28
|
+
label="Auto Translate",
|
|
29
|
+
action_label="Auto Translate",
|
|
30
|
+
description_fields="You will automatically translate all fields available for translation.",
|
|
31
|
+
serializer=AutoTranslateSerializer,
|
|
32
|
+
instance_display=create_simple_display([["override_existing_data"]]),
|
|
33
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from django.conf import settings
|
|
2
|
+
|
|
3
|
+
from wbcore.serializers.fields import JSONField
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TranslationJSONField(JSONField):
|
|
7
|
+
field_type = "translation_field"
|
|
8
|
+
|
|
9
|
+
def __init__(self, *args, **kwargs):
|
|
10
|
+
super().__init__(*args, **kwargs)
|
|
11
|
+
self.read_only = False
|
|
12
|
+
self.required = False
|
|
13
|
+
self.allow_null = True
|
|
14
|
+
|
|
15
|
+
def get_representation(self, request, field_name) -> tuple[str, dict]:
|
|
16
|
+
key, representation = super().get_representation(request, field_name)
|
|
17
|
+
representation["default_language"] = settings.LANGUAGE_CODE
|
|
18
|
+
representation["languages"] = settings.MODELTRANS_AVAILABLE_LANGUAGES
|
|
19
|
+
representation["fields"] = self.parent.Meta.model._meta.get_field("i18n").fields
|
|
20
|
+
return key, representation
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from wbcore import serializers
|
|
2
|
+
from wbcore.contrib.i18n.serializers.fields import TranslationJSONField
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ModelTranslateSerializerMixin(serializers.ModelSerializer):
|
|
6
|
+
_i18n = TranslationJSONField(source="i18n")
|
|
7
|
+
|
|
8
|
+
def update(self, instance, validated_data):
|
|
9
|
+
i18n_data = validated_data.pop("i18n", {}) or {}
|
|
10
|
+
if instance.i18n is None:
|
|
11
|
+
instance.i18n = {}
|
|
12
|
+
instance.i18n.update(i18n_data)
|
|
13
|
+
return super().update(instance, validated_data)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from django.apps import apps
|
|
3
|
+
from django.db.backends.base.base import BaseDatabaseWrapper
|
|
4
|
+
from django.db.models.signals import pre_migrate
|
|
5
|
+
from pytest_factoryboy import register
|
|
6
|
+
from wbcore.contrib.authentication.factories import InternalUserFactory, UserFactory
|
|
7
|
+
from wbcore.contrib.geography.tests.signals import app_pre_migration
|
|
8
|
+
from wbcore.tests.conftest import *
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
pre_migrate.connect(app_pre_migration, sender=apps.get_app_config("geography"))
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from unittest.mock import MagicMock, patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from wbcore.contrib.i18n.viewsets import ModelTranslateMixin
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestModel:
|
|
8
|
+
"""A simple test model for testing the ModelTranslateMixin."""
|
|
9
|
+
|
|
10
|
+
id = 1
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def viewset():
|
|
15
|
+
"""Create a viewset instance for testing."""
|
|
16
|
+
|
|
17
|
+
class TestViewSet(ModelTranslateMixin):
|
|
18
|
+
def get_object(self):
|
|
19
|
+
return TestModel()
|
|
20
|
+
|
|
21
|
+
return TestViewSet()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@patch("wbcore.contrib.i18n.viewsets.translate_model_as_task")
|
|
25
|
+
@patch("wbcore.contrib.i18n.viewsets.ContentType.objects.get_for_model")
|
|
26
|
+
def test_auto_translate_success(mock_get_for_model, mock_translate_task, viewset, rf):
|
|
27
|
+
"""Test that auto_translate initiates a translation task and returns a success response."""
|
|
28
|
+
# Setup
|
|
29
|
+
mock_content_type = MagicMock()
|
|
30
|
+
mock_content_type.id = 123
|
|
31
|
+
mock_get_for_model.return_value = mock_content_type
|
|
32
|
+
|
|
33
|
+
mock_translate_task.delay = MagicMock()
|
|
34
|
+
|
|
35
|
+
request = rf.post("/auto-translate/")
|
|
36
|
+
request.data = {}
|
|
37
|
+
|
|
38
|
+
# Execute
|
|
39
|
+
response = viewset.auto_translate(request)
|
|
40
|
+
|
|
41
|
+
# Assert
|
|
42
|
+
mock_get_for_model.assert_called_once()
|
|
43
|
+
mock_translate_task.delay.assert_called_once_with(123, 1, False)
|
|
44
|
+
|
|
45
|
+
assert response.status_code == 200
|
|
46
|
+
assert response.data == {"__notification": "The translation started in the background."}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@patch("wbcore.contrib.i18n.viewsets.translate_model_as_task")
|
|
50
|
+
def test_auto_translate_no_object(mock_translate_task, viewset, rf):
|
|
51
|
+
"""Test that auto_translate returns an error response when no object is found."""
|
|
52
|
+
# Setup
|
|
53
|
+
viewset.get_object = MagicMock(return_value=None)
|
|
54
|
+
mock_translate_task.delay = MagicMock()
|
|
55
|
+
|
|
56
|
+
request = rf.post("/auto-translate/")
|
|
57
|
+
request.data = {}
|
|
58
|
+
# Execute
|
|
59
|
+
response = viewset.auto_translate(request)
|
|
60
|
+
|
|
61
|
+
# Assert
|
|
62
|
+
mock_translate_task.delay.assert_not_called()
|
|
63
|
+
|
|
64
|
+
# The viewset code uses status.HTTP_400_BAD_REQUEST
|
|
65
|
+
assert response.status_code == 400
|
|
66
|
+
assert "non_field_errors" in response.data
|
|
67
|
+
assert response.data["non_field_errors"] == ["The URL was malformatted."]
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
from copy import deepcopy
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Iterator
|
|
3
|
+
|
|
4
|
+
from celery import shared_task
|
|
5
|
+
from django.conf import settings
|
|
6
|
+
from django.contrib.contenttypes.models import ContentType
|
|
7
|
+
from django.db.models import Field
|
|
8
|
+
from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage
|
|
9
|
+
from modeltrans.fields import TranslatedVirtualField
|
|
10
|
+
|
|
11
|
+
from wbcore.contrib.ai.llm.utils import run_llm
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from django.db.models import Model
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_base_prompt(from_language: str, to_language: str) -> list["BaseMessage"]:
|
|
18
|
+
"""
|
|
19
|
+
Creates a base prompt for translation with system instructions.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
from_language: ISO2 code of the source language
|
|
23
|
+
to_language: ISO2 code of the target language
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
list[BaseMessage]: A list containing the system message for translation
|
|
27
|
+
"""
|
|
28
|
+
return [
|
|
29
|
+
SystemMessage(f"""
|
|
30
|
+
You are a highly skilled translator with expertise in accurately translating content between languages.
|
|
31
|
+
Your task is to translate the provided content from ISO2={from_language} to ISO2={to_language}.
|
|
32
|
+
|
|
33
|
+
Please adhere to the following guidelines:
|
|
34
|
+
- Maintain the original meaning, tone, and style of the content.
|
|
35
|
+
- Preserve any formatting, including line breaks and special characters.
|
|
36
|
+
- Keep proper nouns unchanged unless they have widely accepted translations.
|
|
37
|
+
- Do not add, remove, or alter any information in the content.
|
|
38
|
+
- Provide only the translated text without any additional explanations or notes.
|
|
39
|
+
""")
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _is_translated_field(field: Field | TranslatedVirtualField) -> bool:
|
|
44
|
+
"""
|
|
45
|
+
Determines if a field is a translated field that needs processing.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
field: The field to check
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
bool: True if the field is a TranslatedVirtualField with a non-default language
|
|
52
|
+
"""
|
|
53
|
+
return (
|
|
54
|
+
isinstance(field, TranslatedVirtualField)
|
|
55
|
+
and field.language is not None
|
|
56
|
+
and field.language != settings.LANGUAGE_CODE
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _get_translation_fields(
|
|
61
|
+
fields: list[Field | TranslatedVirtualField | Any],
|
|
62
|
+
) -> Iterator[TranslatedVirtualField]:
|
|
63
|
+
"""
|
|
64
|
+
Filters a list of fields to only include translated fields.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
fields: List of model fields to filter
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Iterator[TranslatedVirtualField]: Iterator yielding only the translated virtual fields
|
|
71
|
+
"""
|
|
72
|
+
yield from filter(_is_translated_field, fields) # type: ignore
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def translate_string(content: str, language: str) -> str:
|
|
76
|
+
prompt = get_base_prompt(settings.LANGUAGE_CODE, language)
|
|
77
|
+
prompt.append(HumanMessage(content))
|
|
78
|
+
return str(run_llm(prompt)[0])
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def translate_dict(content: dict, language: str) -> dict:
|
|
82
|
+
new_content = deepcopy(content)
|
|
83
|
+
|
|
84
|
+
for key, value in content.items():
|
|
85
|
+
if isinstance(value, dict):
|
|
86
|
+
new_content[key] = translate_dict(value, language)
|
|
87
|
+
elif isinstance(value, str):
|
|
88
|
+
new_content[key] = translate_string(value, language)
|
|
89
|
+
|
|
90
|
+
return new_content
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def translate_model(model: "Model", override: bool = False):
|
|
94
|
+
"""
|
|
95
|
+
Translates all translatable fields in a model instance.
|
|
96
|
+
|
|
97
|
+
Processes all fields in the model that are marked for translation,
|
|
98
|
+
generates translations using an LLM, and saves the model with
|
|
99
|
+
the translated content.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
model: The Django model instance to translate
|
|
103
|
+
override: If True, do not set the translated value if there is already content
|
|
104
|
+
"""
|
|
105
|
+
for field in _get_translation_fields(model._meta.get_fields()):
|
|
106
|
+
original_content = getattr(model, field.original_field.name)
|
|
107
|
+
if field.language is None or (field is not None and not override):
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
if custom_translation := getattr(model, f"_translate_{field.original_field.name}", None):
|
|
111
|
+
setattr(model, field.name, custom_translation(field.language))
|
|
112
|
+
else:
|
|
113
|
+
match original_content:
|
|
114
|
+
case str():
|
|
115
|
+
setattr(
|
|
116
|
+
model,
|
|
117
|
+
field.name,
|
|
118
|
+
translate_string(original_content, field.language),
|
|
119
|
+
)
|
|
120
|
+
case dict():
|
|
121
|
+
setattr(model, field.name, translate_dict(original_content, field.language))
|
|
122
|
+
|
|
123
|
+
model.save()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@shared_task
|
|
127
|
+
def translate_model_as_task(content_type_id: int, pk: Any, override: bool = False):
|
|
128
|
+
"""
|
|
129
|
+
Celery task for asynchronous translation of a model instance.
|
|
130
|
+
|
|
131
|
+
Retrieves the model class from the content type ID, loads the
|
|
132
|
+
specific instance by primary key, and translates its fields.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
content_type_id: ID of the ContentType for the model to translate
|
|
136
|
+
pk: Primary key of the specific model instance to translate
|
|
137
|
+
"""
|
|
138
|
+
if mc := ContentType.objects.get(id=content_type_id).model_class():
|
|
139
|
+
model = mc.objects.get(pk=pk)
|
|
140
|
+
translate_model(model, override)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from django.contrib.contenttypes.models import ContentType
|
|
2
|
+
from rest_framework import status
|
|
3
|
+
from rest_framework.decorators import action
|
|
4
|
+
from rest_framework.request import Request
|
|
5
|
+
from rest_framework.response import Response
|
|
6
|
+
|
|
7
|
+
from wbcore import viewsets
|
|
8
|
+
from wbcore.contrib.i18n.translation import translate_model_as_task
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ModelTranslateMixin(viewsets.ModelViewSet):
|
|
12
|
+
@action(methods=["POST"], detail=True)
|
|
13
|
+
def auto_translate(self, request: Request, *args, **kwargs):
|
|
14
|
+
"""Initiates an asynchronous translation task for the specified model instance.
|
|
15
|
+
|
|
16
|
+
This method retrieves the model instance based on the provided URL parameters,
|
|
17
|
+
starts a background translation task, and returns a notification response.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
request (Request): The HTTP request object containing the data for the translation.
|
|
21
|
+
*args: Additional positional arguments.
|
|
22
|
+
**kwargs: Additional keyword arguments.
|
|
23
|
+
override_existing_data (bool): If True, existing content will not be overridden.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Response: A response indicating the status of the translation initiation.
|
|
27
|
+
"""
|
|
28
|
+
override_existing_data = request.data.get("override_existing_data", "false") == "true"
|
|
29
|
+
|
|
30
|
+
obj = self.get_object()
|
|
31
|
+
if obj:
|
|
32
|
+
ct = ContentType.objects.get_for_model(obj) # type: ignore
|
|
33
|
+
translate_model_as_task.delay(ct.id, obj.id, override_existing_data)
|
|
34
|
+
return Response({"__notification": "The translation started in the background."})
|
|
35
|
+
|
|
36
|
+
return Response({"non_field_errors": ["The URL was malformatted."]}, status=status.HTTP_400_BAD_REQUEST)
|
|
@@ -19,6 +19,7 @@ class IconBackend(AbstractBackend):
|
|
|
19
19
|
BANK = "account_balance"
|
|
20
20
|
BIRTHDAY = "cake"
|
|
21
21
|
BOOKMARK = "bookmark_border"
|
|
22
|
+
BROADCAST = "arrow_split"
|
|
22
23
|
CALENDAR = "calendar_month"
|
|
23
24
|
CHART_AREA = "stacked_line_chart"
|
|
24
25
|
CHART_BARS_HORIZONTAL = "align_horizontal_center"
|
wbcore/contrib/icons/icons.py
CHANGED
|
@@ -15,20 +15,16 @@ FALLBACK_ICON_VALUE = "FALLBACK_ICON"
|
|
|
15
15
|
class WBIconMeta(ChoicesMeta):
|
|
16
16
|
"""A metaclass for creating a enum choices."""
|
|
17
17
|
|
|
18
|
-
def __new__(
|
|
18
|
+
def __new__(cls, classname, bases, classdict, **kwds):
|
|
19
19
|
dict.__setitem__(
|
|
20
20
|
classdict, FALLBACK_ICON_VALUE, (FALLBACK_ICON_VALUE, "Fallback Icon")
|
|
21
21
|
) # add a default member that represent the fallback in case it's not yet implement in the imported backend
|
|
22
|
-
cls = super().__new__(
|
|
22
|
+
cls = super().__new__(cls, classname, bases, classdict, **kwds)
|
|
23
23
|
with suppress(ModuleNotFoundError):
|
|
24
|
-
|
|
25
|
-
cls,
|
|
26
|
-
"icon_backend",
|
|
27
|
-
import_from_dotted_path(getattr(settings, "WBCORE_ICON_BACKEND", DEFAULT_ICON_BACKEND)),
|
|
28
|
-
)
|
|
24
|
+
cls.icon_backend = import_from_dotted_path(getattr(settings, "WBCORE_ICON_BACKEND", DEFAULT_ICON_BACKEND))
|
|
29
25
|
icon_backend = cls.icon_backend
|
|
30
26
|
# For each enumeration values, attached an "icon" property to its members
|
|
31
|
-
for member, value in zip(cls.__members__.values(), cls.values):
|
|
27
|
+
for member, value in zip(cls.__members__.values(), cls.values, strict=False):
|
|
32
28
|
member._icon_ = getattr(icon_backend, value, icon_backend.fallback_icon)
|
|
33
29
|
return enum.unique(cls)
|
|
34
30
|
|
|
@@ -54,6 +50,7 @@ class WBIcon(TextChoices, metaclass=WBIconMeta):
|
|
|
54
50
|
BANK = "BANK", "Bank"
|
|
55
51
|
BIRTHDAY = "BIRTHDAY", "Birthday"
|
|
56
52
|
BOOKMARK = "BOOKMARK", "Bookmark"
|
|
53
|
+
BROADCAST = "BROADCAST", "Broadcast"
|
|
57
54
|
CALENDAR = "CALENDAR", "Calendar"
|
|
58
55
|
CHART_AREA = "CHART_AREA", "Chart area"
|
|
59
56
|
CHART_BARS_HORIZONTAL = "CHART_BARS_HORIZONTAL", "Chart bars horizonal"
|
wbcore/contrib/io/admin.py
CHANGED