udata 9.1.2.dev30355__py2.py3-none-any.whl → 9.1.2.dev30454__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.
Potentially problematic release.
This version of udata might be problematic. Click here for more details.
- tasks/__init__.py +109 -107
- tasks/helpers.py +18 -18
- udata/__init__.py +4 -4
- udata/admin/views.py +5 -5
- udata/api/__init__.py +111 -134
- udata/api/commands.py +45 -37
- udata/api/errors.py +5 -4
- udata/api/fields.py +23 -21
- udata/api/oauth2.py +55 -74
- udata/api/parsers.py +15 -15
- udata/api/signals.py +1 -1
- udata/api_fields.py +137 -89
- udata/app.py +58 -55
- udata/assets.py +5 -5
- udata/auth/__init__.py +37 -26
- udata/auth/forms.py +23 -15
- udata/auth/helpers.py +1 -1
- udata/auth/mails.py +3 -3
- udata/auth/password_validation.py +19 -15
- udata/auth/views.py +94 -68
- udata/commands/__init__.py +71 -69
- udata/commands/cache.py +7 -7
- udata/commands/db.py +201 -140
- udata/commands/dcat.py +36 -30
- udata/commands/fixtures.py +100 -84
- udata/commands/images.py +21 -20
- udata/commands/info.py +17 -20
- udata/commands/init.py +10 -10
- udata/commands/purge.py +12 -13
- udata/commands/serve.py +41 -29
- udata/commands/static.py +16 -18
- udata/commands/test.py +20 -20
- udata/commands/tests/fixtures.py +26 -24
- udata/commands/worker.py +31 -33
- udata/core/__init__.py +12 -12
- udata/core/activity/__init__.py +0 -1
- udata/core/activity/api.py +59 -49
- udata/core/activity/models.py +28 -26
- udata/core/activity/signals.py +1 -1
- udata/core/activity/tasks.py +16 -10
- udata/core/badges/api.py +6 -6
- udata/core/badges/commands.py +14 -13
- udata/core/badges/fields.py +8 -5
- udata/core/badges/forms.py +7 -4
- udata/core/badges/models.py +16 -31
- udata/core/badges/permissions.py +1 -3
- udata/core/badges/signals.py +2 -2
- udata/core/badges/tasks.py +3 -2
- udata/core/badges/tests/test_commands.py +10 -10
- udata/core/badges/tests/test_model.py +24 -31
- udata/core/contact_point/api.py +19 -18
- udata/core/contact_point/api_fields.py +21 -14
- udata/core/contact_point/factories.py +2 -2
- udata/core/contact_point/forms.py +7 -6
- udata/core/contact_point/models.py +3 -5
- udata/core/dataservices/api.py +26 -21
- udata/core/dataservices/factories.py +13 -11
- udata/core/dataservices/models.py +35 -40
- udata/core/dataservices/permissions.py +4 -4
- udata/core/dataservices/rdf.py +40 -17
- udata/core/dataservices/tasks.py +4 -3
- udata/core/dataset/actions.py +10 -10
- udata/core/dataset/activities.py +21 -23
- udata/core/dataset/api.py +321 -298
- udata/core/dataset/api_fields.py +443 -271
- udata/core/dataset/apiv2.py +305 -229
- udata/core/dataset/commands.py +38 -36
- udata/core/dataset/constants.py +61 -54
- udata/core/dataset/csv.py +70 -74
- udata/core/dataset/events.py +39 -32
- udata/core/dataset/exceptions.py +8 -4
- udata/core/dataset/factories.py +57 -65
- udata/core/dataset/forms.py +87 -63
- udata/core/dataset/models.py +336 -280
- udata/core/dataset/permissions.py +9 -6
- udata/core/dataset/preview.py +15 -17
- udata/core/dataset/rdf.py +156 -122
- udata/core/dataset/search.py +92 -77
- udata/core/dataset/signals.py +1 -1
- udata/core/dataset/tasks.py +63 -54
- udata/core/discussions/actions.py +5 -5
- udata/core/discussions/api.py +124 -120
- udata/core/discussions/factories.py +2 -2
- udata/core/discussions/forms.py +9 -7
- udata/core/discussions/metrics.py +1 -3
- udata/core/discussions/models.py +25 -24
- udata/core/discussions/notifications.py +18 -14
- udata/core/discussions/permissions.py +3 -3
- udata/core/discussions/signals.py +4 -4
- udata/core/discussions/tasks.py +24 -28
- udata/core/followers/api.py +32 -33
- udata/core/followers/models.py +9 -9
- udata/core/followers/signals.py +3 -3
- udata/core/jobs/actions.py +7 -7
- udata/core/jobs/api.py +99 -92
- udata/core/jobs/commands.py +48 -49
- udata/core/jobs/forms.py +11 -11
- udata/core/jobs/models.py +6 -6
- udata/core/metrics/__init__.py +2 -2
- udata/core/metrics/commands.py +34 -30
- udata/core/metrics/models.py +2 -4
- udata/core/metrics/signals.py +1 -1
- udata/core/metrics/tasks.py +3 -3
- udata/core/organization/activities.py +12 -15
- udata/core/organization/api.py +167 -174
- udata/core/organization/api_fields.py +183 -124
- udata/core/organization/apiv2.py +32 -32
- udata/core/organization/commands.py +20 -22
- udata/core/organization/constants.py +11 -11
- udata/core/organization/csv.py +17 -15
- udata/core/organization/factories.py +8 -11
- udata/core/organization/forms.py +32 -26
- udata/core/organization/metrics.py +2 -1
- udata/core/organization/models.py +87 -67
- udata/core/organization/notifications.py +18 -14
- udata/core/organization/permissions.py +10 -11
- udata/core/organization/rdf.py +14 -14
- udata/core/organization/search.py +30 -28
- udata/core/organization/signals.py +7 -7
- udata/core/organization/tasks.py +42 -61
- udata/core/owned.py +38 -27
- udata/core/post/api.py +82 -81
- udata/core/post/constants.py +8 -5
- udata/core/post/factories.py +4 -4
- udata/core/post/forms.py +13 -14
- udata/core/post/models.py +20 -22
- udata/core/post/tests/test_api.py +30 -32
- udata/core/reports/api.py +8 -7
- udata/core/reports/constants.py +1 -3
- udata/core/reports/models.py +10 -10
- udata/core/reuse/activities.py +15 -19
- udata/core/reuse/api.py +123 -126
- udata/core/reuse/api_fields.py +120 -85
- udata/core/reuse/apiv2.py +11 -10
- udata/core/reuse/constants.py +23 -23
- udata/core/reuse/csv.py +18 -18
- udata/core/reuse/factories.py +5 -9
- udata/core/reuse/forms.py +24 -21
- udata/core/reuse/models.py +55 -51
- udata/core/reuse/permissions.py +2 -2
- udata/core/reuse/search.py +49 -46
- udata/core/reuse/signals.py +1 -1
- udata/core/reuse/tasks.py +4 -5
- udata/core/site/api.py +47 -50
- udata/core/site/factories.py +2 -2
- udata/core/site/forms.py +4 -5
- udata/core/site/models.py +94 -63
- udata/core/site/rdf.py +14 -14
- udata/core/spam/api.py +16 -9
- udata/core/spam/constants.py +4 -4
- udata/core/spam/fields.py +13 -7
- udata/core/spam/models.py +27 -20
- udata/core/spam/signals.py +1 -1
- udata/core/spam/tests/test_spam.py +6 -5
- udata/core/spatial/api.py +72 -80
- udata/core/spatial/api_fields.py +73 -58
- udata/core/spatial/commands.py +67 -64
- udata/core/spatial/constants.py +3 -3
- udata/core/spatial/factories.py +37 -54
- udata/core/spatial/forms.py +27 -26
- udata/core/spatial/geoids.py +17 -17
- udata/core/spatial/models.py +43 -47
- udata/core/spatial/tasks.py +2 -1
- udata/core/spatial/tests/test_api.py +115 -130
- udata/core/spatial/tests/test_fields.py +74 -77
- udata/core/spatial/tests/test_geoid.py +22 -22
- udata/core/spatial/tests/test_models.py +5 -7
- udata/core/spatial/translations.py +16 -16
- udata/core/storages/__init__.py +16 -18
- udata/core/storages/api.py +66 -64
- udata/core/storages/tasks.py +7 -7
- udata/core/storages/utils.py +15 -15
- udata/core/storages/views.py +5 -6
- udata/core/tags/api.py +17 -14
- udata/core/tags/csv.py +4 -4
- udata/core/tags/models.py +8 -5
- udata/core/tags/tasks.py +11 -13
- udata/core/tags/views.py +4 -4
- udata/core/topic/api.py +84 -73
- udata/core/topic/apiv2.py +157 -127
- udata/core/topic/factories.py +3 -4
- udata/core/topic/forms.py +12 -14
- udata/core/topic/models.py +14 -19
- udata/core/topic/parsers.py +26 -26
- udata/core/user/activities.py +30 -29
- udata/core/user/api.py +151 -152
- udata/core/user/api_fields.py +132 -100
- udata/core/user/apiv2.py +7 -7
- udata/core/user/commands.py +38 -38
- udata/core/user/factories.py +8 -9
- udata/core/user/forms.py +14 -11
- udata/core/user/metrics.py +2 -2
- udata/core/user/models.py +68 -69
- udata/core/user/permissions.py +4 -5
- udata/core/user/rdf.py +7 -8
- udata/core/user/tasks.py +2 -2
- udata/core/user/tests/test_user_model.py +24 -16
- udata/cors.py +99 -0
- udata/db/tasks.py +2 -1
- udata/entrypoints.py +35 -31
- udata/errors.py +2 -1
- udata/event/values.py +6 -6
- udata/factories.py +2 -2
- udata/features/identicon/api.py +5 -6
- udata/features/identicon/backends.py +48 -55
- udata/features/identicon/tests/test_backends.py +4 -5
- udata/features/notifications/__init__.py +0 -1
- udata/features/notifications/actions.py +9 -9
- udata/features/notifications/api.py +17 -13
- udata/features/territories/__init__.py +12 -10
- udata/features/territories/api.py +14 -15
- udata/features/territories/models.py +23 -28
- udata/features/transfer/actions.py +8 -11
- udata/features/transfer/api.py +84 -77
- udata/features/transfer/factories.py +2 -1
- udata/features/transfer/models.py +11 -12
- udata/features/transfer/notifications.py +19 -15
- udata/features/transfer/permissions.py +5 -5
- udata/forms/__init__.py +5 -2
- udata/forms/fields.py +164 -172
- udata/forms/validators.py +19 -22
- udata/forms/widgets.py +9 -13
- udata/frontend/__init__.py +31 -26
- udata/frontend/csv.py +68 -58
- udata/frontend/markdown.py +40 -44
- udata/harvest/actions.py +89 -77
- udata/harvest/api.py +294 -238
- udata/harvest/backends/__init__.py +4 -4
- udata/harvest/backends/base.py +128 -111
- udata/harvest/backends/dcat.py +80 -66
- udata/harvest/commands.py +56 -60
- udata/harvest/csv.py +8 -8
- udata/harvest/exceptions.py +6 -3
- udata/harvest/filters.py +24 -23
- udata/harvest/forms.py +27 -28
- udata/harvest/models.py +88 -80
- udata/harvest/notifications.py +15 -10
- udata/harvest/signals.py +13 -13
- udata/harvest/tasks.py +11 -10
- udata/harvest/tests/factories.py +23 -24
- udata/harvest/tests/test_actions.py +136 -166
- udata/harvest/tests/test_api.py +220 -214
- udata/harvest/tests/test_base_backend.py +117 -112
- udata/harvest/tests/test_dcat_backend.py +380 -308
- udata/harvest/tests/test_filters.py +33 -22
- udata/harvest/tests/test_models.py +11 -14
- udata/harvest/tests/test_notifications.py +6 -7
- udata/harvest/tests/test_tasks.py +7 -6
- udata/i18n.py +237 -78
- udata/linkchecker/backends.py +5 -11
- udata/linkchecker/checker.py +23 -22
- udata/linkchecker/commands.py +4 -6
- udata/linkchecker/models.py +6 -6
- udata/linkchecker/tasks.py +18 -20
- udata/mail.py +21 -21
- udata/migrations/2020-07-24-remove-s-from-scope-oauth.py +9 -8
- udata/migrations/2020-08-24-add-fs-filename.py +9 -8
- udata/migrations/2020-09-28-update-reuses-datasets-metrics.py +5 -4
- udata/migrations/2020-10-16-migrate-ods-resources.py +9 -10
- udata/migrations/2021-04-08-update-schema-with-new-structure.py +8 -7
- udata/migrations/2021-05-27-fix-default-schema-name.py +7 -6
- udata/migrations/2021-07-05-remove-unused-badges.py +17 -15
- udata/migrations/2021-07-07-update-schema-for-community-resources.py +7 -6
- udata/migrations/2021-08-17-follow-integrity.py +5 -4
- udata/migrations/2021-08-17-harvest-integrity.py +13 -12
- udata/migrations/2021-08-17-oauth2client-integrity.py +5 -4
- udata/migrations/2021-08-17-transfer-integrity.py +5 -4
- udata/migrations/2021-08-17-users-integrity.py +9 -8
- udata/migrations/2021-12-14-reuse-topics.py +7 -6
- udata/migrations/2022-04-21-improve-extension-detection.py +8 -7
- udata/migrations/2022-09-22-clean-inactive-harvest-datasets.py +16 -14
- udata/migrations/2022-10-10-add-fs_uniquifier-to-user-model.py +6 -6
- udata/migrations/2022-10-10-migrate-harvest-extras.py +36 -26
- udata/migrations/2023-02-08-rename-internal-dates.py +46 -28
- udata/migrations/2024-01-29-fix-reuse-and-dataset-with-private-None.py +10 -8
- udata/migrations/2024-03-22-migrate-activity-kwargs-to-extras.py +6 -4
- udata/migrations/2024-06-11-fix-reuse-datasets-references.py +7 -6
- udata/migrations/__init__.py +123 -105
- udata/models/__init__.py +4 -4
- udata/mongo/__init__.py +13 -11
- udata/mongo/badges_field.py +3 -2
- udata/mongo/datetime_fields.py +13 -12
- udata/mongo/document.py +17 -16
- udata/mongo/engine.py +15 -16
- udata/mongo/errors.py +2 -1
- udata/mongo/extras_fields.py +30 -20
- udata/mongo/queryset.py +12 -12
- udata/mongo/slug_fields.py +38 -28
- udata/mongo/taglist_field.py +1 -2
- udata/mongo/url_field.py +5 -5
- udata/mongo/uuid_fields.py +4 -3
- udata/notifications/__init__.py +1 -1
- udata/notifications/mattermost.py +10 -9
- udata/rdf.py +167 -188
- udata/routing.py +40 -45
- udata/search/__init__.py +18 -19
- udata/search/adapter.py +17 -16
- udata/search/commands.py +44 -51
- udata/search/fields.py +13 -20
- udata/search/query.py +23 -18
- udata/search/result.py +9 -10
- udata/sentry.py +21 -19
- udata/settings.py +262 -198
- udata/sitemap.py +8 -6
- udata/storage/s3.py +20 -13
- udata/tags.py +4 -5
- udata/tasks.py +43 -42
- udata/tests/__init__.py +9 -6
- udata/tests/api/__init__.py +8 -6
- udata/tests/api/test_auth_api.py +395 -321
- udata/tests/api/test_base_api.py +33 -35
- udata/tests/api/test_contact_points.py +7 -9
- udata/tests/api/test_dataservices_api.py +211 -158
- udata/tests/api/test_datasets_api.py +823 -812
- udata/tests/api/test_follow_api.py +13 -15
- udata/tests/api/test_me_api.py +95 -112
- udata/tests/api/test_organizations_api.py +301 -339
- udata/tests/api/test_reports_api.py +35 -25
- udata/tests/api/test_reuses_api.py +134 -139
- udata/tests/api/test_swagger.py +5 -5
- udata/tests/api/test_tags_api.py +18 -25
- udata/tests/api/test_topics_api.py +94 -94
- udata/tests/api/test_transfer_api.py +53 -48
- udata/tests/api/test_user_api.py +128 -141
- udata/tests/apiv2/test_datasets.py +290 -198
- udata/tests/apiv2/test_me_api.py +10 -11
- udata/tests/apiv2/test_organizations.py +56 -74
- udata/tests/apiv2/test_swagger.py +5 -5
- udata/tests/apiv2/test_topics.py +69 -87
- udata/tests/cli/test_cli_base.py +8 -8
- udata/tests/cli/test_db_cli.py +21 -19
- udata/tests/dataservice/test_dataservice_tasks.py +8 -12
- udata/tests/dataset/test_csv_adapter.py +44 -35
- udata/tests/dataset/test_dataset_actions.py +2 -3
- udata/tests/dataset/test_dataset_commands.py +7 -8
- udata/tests/dataset/test_dataset_events.py +36 -29
- udata/tests/dataset/test_dataset_model.py +224 -217
- udata/tests/dataset/test_dataset_rdf.py +142 -131
- udata/tests/dataset/test_dataset_tasks.py +15 -15
- udata/tests/dataset/test_resource_preview.py +10 -13
- udata/tests/features/territories/__init__.py +9 -13
- udata/tests/features/territories/test_territories_api.py +71 -91
- udata/tests/forms/test_basic_fields.py +7 -7
- udata/tests/forms/test_current_user_field.py +39 -66
- udata/tests/forms/test_daterange_field.py +31 -39
- udata/tests/forms/test_dict_field.py +28 -26
- udata/tests/forms/test_extras_fields.py +102 -76
- udata/tests/forms/test_form_field.py +8 -8
- udata/tests/forms/test_image_field.py +33 -26
- udata/tests/forms/test_model_field.py +134 -123
- udata/tests/forms/test_model_list_field.py +7 -7
- udata/tests/forms/test_nested_model_list_field.py +117 -79
- udata/tests/forms/test_publish_as_field.py +36 -65
- udata/tests/forms/test_reference_field.py +34 -53
- udata/tests/forms/test_user_forms.py +23 -21
- udata/tests/forms/test_uuid_field.py +6 -10
- udata/tests/frontend/__init__.py +9 -6
- udata/tests/frontend/test_auth.py +7 -6
- udata/tests/frontend/test_csv.py +81 -96
- udata/tests/frontend/test_hooks.py +43 -43
- udata/tests/frontend/test_markdown.py +211 -191
- udata/tests/helpers.py +32 -37
- udata/tests/models.py +2 -2
- udata/tests/organization/test_csv_adapter.py +21 -16
- udata/tests/organization/test_notifications.py +11 -18
- udata/tests/organization/test_organization_model.py +13 -13
- udata/tests/organization/test_organization_rdf.py +29 -22
- udata/tests/organization/test_organization_tasks.py +16 -17
- udata/tests/plugin.py +79 -73
- udata/tests/reuse/test_reuse_model.py +21 -21
- udata/tests/reuse/test_reuse_task.py +11 -13
- udata/tests/search/__init__.py +11 -12
- udata/tests/search/test_adapter.py +60 -70
- udata/tests/search/test_query.py +16 -16
- udata/tests/search/test_results.py +10 -7
- udata/tests/site/test_site_api.py +11 -16
- udata/tests/site/test_site_metrics.py +20 -30
- udata/tests/site/test_site_model.py +4 -5
- udata/tests/site/test_site_rdf.py +94 -78
- udata/tests/test_activity.py +17 -17
- udata/tests/test_cors.py +62 -0
- udata/tests/test_discussions.py +292 -299
- udata/tests/test_i18n.py +37 -40
- udata/tests/test_linkchecker.py +91 -85
- udata/tests/test_mail.py +13 -17
- udata/tests/test_migrations.py +219 -180
- udata/tests/test_model.py +164 -157
- udata/tests/test_notifications.py +17 -17
- udata/tests/test_owned.py +14 -14
- udata/tests/test_rdf.py +25 -23
- udata/tests/test_routing.py +89 -93
- udata/tests/test_storages.py +137 -128
- udata/tests/test_tags.py +44 -46
- udata/tests/test_topics.py +7 -7
- udata/tests/test_transfer.py +42 -49
- udata/tests/test_uris.py +160 -161
- udata/tests/test_utils.py +79 -71
- udata/tests/user/test_user_rdf.py +5 -9
- udata/tests/workers/test_jobs_commands.py +57 -58
- udata/tests/workers/test_tasks_routing.py +23 -29
- udata/tests/workers/test_workers_api.py +125 -131
- udata/tests/workers/test_workers_helpers.py +6 -6
- udata/tracking.py +4 -6
- udata/uris.py +45 -46
- udata/utils.py +68 -66
- udata/wsgi.py +1 -1
- {udata-9.1.2.dev30355.dist-info → udata-9.1.2.dev30454.dist-info}/METADATA +7 -3
- udata-9.1.2.dev30454.dist-info/RECORD +706 -0
- udata-9.1.2.dev30355.dist-info/RECORD +0 -704
- {udata-9.1.2.dev30355.dist-info → udata-9.1.2.dev30454.dist-info}/LICENSE +0 -0
- {udata-9.1.2.dev30355.dist-info → udata-9.1.2.dev30454.dist-info}/WHEEL +0 -0
- {udata-9.1.2.dev30355.dist-info → udata-9.1.2.dev30454.dist-info}/entry_points.txt +0 -0
- {udata-9.1.2.dev30355.dist-info → udata-9.1.2.dev30454.dist-info}/top_level.txt +0 -0
udata/api/__init__.py
CHANGED
|
@@ -1,89 +1,53 @@
|
|
|
1
|
-
import itertools
|
|
2
1
|
import inspect
|
|
2
|
+
import itertools
|
|
3
3
|
import logging
|
|
4
4
|
import urllib.parse
|
|
5
|
-
|
|
6
5
|
from functools import wraps
|
|
7
6
|
from importlib import import_module
|
|
8
7
|
|
|
9
8
|
from flask import (
|
|
10
|
-
|
|
9
|
+
Blueprint,
|
|
10
|
+
current_app,
|
|
11
|
+
g,
|
|
12
|
+
json,
|
|
13
|
+
make_response,
|
|
14
|
+
redirect,
|
|
15
|
+
request,
|
|
16
|
+
url_for,
|
|
11
17
|
)
|
|
12
|
-
from flask_storage import UnauthorizedFileType
|
|
13
18
|
from flask_restx import Api, Resource
|
|
14
|
-
from
|
|
19
|
+
from flask_storage import UnauthorizedFileType
|
|
15
20
|
|
|
16
|
-
from udata import
|
|
21
|
+
from udata import cors, entrypoints, tracking
|
|
17
22
|
from udata.app import csrf
|
|
23
|
+
from udata.auth import Permission, PermissionDenied, RoleNeed, current_user, login_user
|
|
18
24
|
from udata.i18n import get_locale
|
|
19
|
-
from udata.auth import (
|
|
20
|
-
current_user, login_user, Permission, RoleNeed, PermissionDenied
|
|
21
|
-
)
|
|
22
|
-
from udata.utils import safe_unicode
|
|
23
25
|
from udata.mongo.errors import FieldValidationError
|
|
26
|
+
from udata.utils import safe_unicode
|
|
24
27
|
|
|
25
28
|
from . import fields
|
|
26
29
|
from .signals import on_api_call
|
|
27
30
|
|
|
28
|
-
|
|
29
31
|
log = logging.getLogger(__name__)
|
|
30
32
|
|
|
31
|
-
apiv1_blueprint = Blueprint(
|
|
32
|
-
apiv2_blueprint = Blueprint(
|
|
33
|
+
apiv1_blueprint = Blueprint("api", __name__, url_prefix="/api/1")
|
|
34
|
+
apiv2_blueprint = Blueprint("apiv2", __name__, url_prefix="/api/2")
|
|
33
35
|
|
|
34
36
|
DEFAULT_PAGE_SIZE = 50
|
|
35
|
-
HEADER_API_KEY =
|
|
36
|
-
|
|
37
|
-
# TODO: make upstream flask-restplus automatically handle
|
|
38
|
-
# flask-restplus headers and allow lazy evaluation
|
|
39
|
-
# of headers (ie. callable)
|
|
40
|
-
PREFLIGHT_HEADERS = (
|
|
41
|
-
HEADER_API_KEY,
|
|
42
|
-
'X-Fields',
|
|
43
|
-
'Content-Type',
|
|
44
|
-
'Accept',
|
|
45
|
-
'Accept-Charset',
|
|
46
|
-
'Accept-Language',
|
|
47
|
-
'Authorization',
|
|
48
|
-
'Cache-Control',
|
|
49
|
-
'Content-Encoding',
|
|
50
|
-
'Content-Length',
|
|
51
|
-
'Content-Security-Policy',
|
|
52
|
-
'Content-Type',
|
|
53
|
-
'Cookie',
|
|
54
|
-
'ETag',
|
|
55
|
-
'Host',
|
|
56
|
-
'If-Modified-Since',
|
|
57
|
-
'Keep-Alive',
|
|
58
|
-
'Last-Modified',
|
|
59
|
-
'Origin',
|
|
60
|
-
'Referer',
|
|
61
|
-
'User-Agent',
|
|
62
|
-
'X-Forwarded-For',
|
|
63
|
-
'X-Forwarded-Port',
|
|
64
|
-
'X-Forwarded-Proto',
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
cors = CORS(allow_headers=PREFLIGHT_HEADERS)
|
|
37
|
+
HEADER_API_KEY = "X-API-KEY"
|
|
68
38
|
|
|
69
39
|
|
|
70
40
|
class UDataApi(Api):
|
|
71
41
|
def __init__(self, app=None, **kwargs):
|
|
72
|
-
decorators = kwargs.pop(
|
|
73
|
-
kwargs[
|
|
42
|
+
decorators = kwargs.pop("decorators", []) or []
|
|
43
|
+
kwargs["decorators"] = [self.authentify] + decorators
|
|
74
44
|
super(UDataApi, self).__init__(app, **kwargs)
|
|
75
|
-
self.authorizations = {
|
|
76
|
-
'apikey': {
|
|
77
|
-
'type': 'apiKey',
|
|
78
|
-
'in': 'header',
|
|
79
|
-
'name': HEADER_API_KEY
|
|
80
|
-
}
|
|
81
|
-
}
|
|
45
|
+
self.authorizations = {"apikey": {"type": "apiKey", "in": "header", "name": HEADER_API_KEY}}
|
|
82
46
|
|
|
83
47
|
def secure(self, func):
|
|
84
|
-
|
|
48
|
+
"""Enforce authentication on a given method/verb
|
|
85
49
|
and optionally check a given permission
|
|
86
|
-
|
|
50
|
+
"""
|
|
87
51
|
if isinstance(func, str):
|
|
88
52
|
return self._apply_permission(Permission(RoleNeed(func)))
|
|
89
53
|
elif isinstance(func, Permission):
|
|
@@ -94,21 +58,25 @@ class UDataApi(Api):
|
|
|
94
58
|
def _apply_permission(self, permission):
|
|
95
59
|
def wrapper(func):
|
|
96
60
|
return self._apply_secure(func, permission)
|
|
61
|
+
|
|
97
62
|
return wrapper
|
|
98
63
|
|
|
99
64
|
def _apply_secure(self, func, permission=None):
|
|
100
|
-
|
|
101
|
-
self._build_doc(func, {
|
|
65
|
+
"""Enforce authentication on a given method/verb"""
|
|
66
|
+
self._build_doc(func, {"security": "apikey"})
|
|
102
67
|
|
|
103
68
|
@wraps(func)
|
|
104
69
|
def wrapper(*args, **kwargs):
|
|
105
70
|
if (
|
|
106
|
-
not current_user.is_anonymous
|
|
107
|
-
not current_user.sysadmin
|
|
108
|
-
current_app.config[
|
|
109
|
-
any(ext in str(func) for ext in current_app.config[
|
|
71
|
+
not current_user.is_anonymous
|
|
72
|
+
and not current_user.sysadmin
|
|
73
|
+
and current_app.config["READ_ONLY_MODE"]
|
|
74
|
+
and any(ext in str(func) for ext in current_app.config["METHOD_BLOCKLIST"])
|
|
110
75
|
):
|
|
111
|
-
self.abort(
|
|
76
|
+
self.abort(
|
|
77
|
+
423,
|
|
78
|
+
"Due to security reasons, the creation of new content is currently disabled.",
|
|
79
|
+
)
|
|
112
80
|
|
|
113
81
|
if not current_user.is_authenticated:
|
|
114
82
|
self.abort(401)
|
|
@@ -125,11 +93,12 @@ class UDataApi(Api):
|
|
|
125
93
|
return wrapper
|
|
126
94
|
|
|
127
95
|
def authentify(self, func):
|
|
128
|
-
|
|
96
|
+
"""Authentify the user if credentials are given"""
|
|
97
|
+
|
|
129
98
|
@wraps(func)
|
|
130
99
|
def wrapper(*args, **kwargs):
|
|
131
|
-
from udata.core.user.models import User
|
|
132
100
|
from udata.api.oauth2 import check_credentials
|
|
101
|
+
from udata.core.user.models import User
|
|
133
102
|
|
|
134
103
|
if current_user.is_authenticated:
|
|
135
104
|
return func(*args, **kwargs)
|
|
@@ -139,72 +108,79 @@ class UDataApi(Api):
|
|
|
139
108
|
try:
|
|
140
109
|
user = User.objects.get(apikey=apikey)
|
|
141
110
|
except User.DoesNotExist:
|
|
142
|
-
self.abort(401,
|
|
111
|
+
self.abort(401, "Invalid API Key")
|
|
143
112
|
|
|
144
113
|
if not login_user(user, False):
|
|
145
|
-
self.abort(401,
|
|
114
|
+
self.abort(401, "Inactive user")
|
|
146
115
|
else:
|
|
147
116
|
check_credentials()
|
|
148
117
|
return func(*args, **kwargs)
|
|
118
|
+
|
|
149
119
|
return wrapper
|
|
150
120
|
|
|
151
121
|
def validate(self, form_cls, obj=None):
|
|
152
|
-
|
|
153
|
-
if
|
|
154
|
-
errors = {
|
|
122
|
+
"""Validate a form from the request and handle errors"""
|
|
123
|
+
if "application/json" not in request.headers.get("Content-Type", ""):
|
|
124
|
+
errors = {"Content-Type": "expecting application/json"}
|
|
155
125
|
self.abort(400, errors=errors)
|
|
156
|
-
form = form_cls.from_json(request.json, obj=obj, instance=obj,
|
|
157
|
-
meta={'csrf': False})
|
|
126
|
+
form = form_cls.from_json(request.json, obj=obj, instance=obj, meta={"csrf": False})
|
|
158
127
|
if not form.validate():
|
|
159
128
|
self.abort(400, errors=form.errors)
|
|
160
129
|
return form
|
|
161
130
|
|
|
162
131
|
def render_ui(self):
|
|
163
|
-
return redirect(current_app.config.get(
|
|
132
|
+
return redirect(current_app.config.get("API_DOC_EXTERNAL_LINK"))
|
|
164
133
|
|
|
165
134
|
def unauthorized(self, response):
|
|
166
|
-
|
|
167
|
-
realm = current_app.config.get(
|
|
135
|
+
"""Override to change the WWW-Authenticate challenge"""
|
|
136
|
+
realm = current_app.config.get("HTTP_OAUTH_REALM", "uData")
|
|
168
137
|
challenge = 'Bearer realm="{0}"'.format(realm)
|
|
169
138
|
|
|
170
|
-
response.headers[
|
|
139
|
+
response.headers["WWW-Authenticate"] = challenge
|
|
171
140
|
return response
|
|
172
141
|
|
|
173
142
|
def page_parser(self):
|
|
174
143
|
parser = self.parser()
|
|
175
|
-
parser.add_argument(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
144
|
+
parser.add_argument("page", type=int, default=1, location="args", help="The page to fetch")
|
|
145
|
+
parser.add_argument(
|
|
146
|
+
"page_size", type=int, default=20, location="args", help="The page size to fetch"
|
|
147
|
+
)
|
|
179
148
|
return parser
|
|
180
149
|
|
|
181
150
|
|
|
182
151
|
api = UDataApi(
|
|
183
152
|
apiv1_blueprint,
|
|
184
153
|
decorators=[csrf.exempt],
|
|
185
|
-
version=
|
|
186
|
-
|
|
187
|
-
|
|
154
|
+
version="1.0",
|
|
155
|
+
title="uData API",
|
|
156
|
+
description="uData API",
|
|
157
|
+
default="site",
|
|
158
|
+
default_label="Site global namespace",
|
|
188
159
|
)
|
|
189
160
|
|
|
190
161
|
apiv2 = UDataApi(
|
|
191
162
|
apiv2_blueprint,
|
|
192
163
|
decorators=[csrf.exempt],
|
|
193
|
-
version=
|
|
194
|
-
|
|
195
|
-
|
|
164
|
+
version="2.0",
|
|
165
|
+
title="uData API",
|
|
166
|
+
description="udata API v2",
|
|
167
|
+
default="site",
|
|
168
|
+
default_label="Site global namespace",
|
|
196
169
|
)
|
|
197
170
|
|
|
198
171
|
|
|
199
|
-
api.model_reference = api.model(
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
172
|
+
api.model_reference = api.model(
|
|
173
|
+
"ModelReference",
|
|
174
|
+
{
|
|
175
|
+
"class": fields.ClassName(description="The model class", required=True),
|
|
176
|
+
"id": fields.String(description="The object identifier", required=True),
|
|
177
|
+
},
|
|
178
|
+
)
|
|
203
179
|
|
|
204
180
|
|
|
205
|
-
@api.representation(
|
|
181
|
+
@api.representation("application/json")
|
|
206
182
|
def output_json(data, code, headers=None):
|
|
207
|
-
|
|
183
|
+
"""Use Flask JSON to serialize"""
|
|
208
184
|
resp = make_response(json.dumps(data), code)
|
|
209
185
|
resp.headers.extend(headers or {})
|
|
210
186
|
return resp
|
|
@@ -213,8 +189,8 @@ def output_json(data, code, headers=None):
|
|
|
213
189
|
@apiv1_blueprint.before_request
|
|
214
190
|
@apiv2_blueprint.before_request
|
|
215
191
|
def set_api_language():
|
|
216
|
-
if
|
|
217
|
-
g.lang_code = request.args[
|
|
192
|
+
if "lang" in request.args:
|
|
193
|
+
g.lang_code = request.args["lang"]
|
|
218
194
|
else:
|
|
219
195
|
g.lang_code = get_locale()
|
|
220
196
|
|
|
@@ -225,17 +201,18 @@ def extract_name_from_path(path):
|
|
|
225
201
|
Useful to log requests on Piwik with categories tree structure.
|
|
226
202
|
See: http://piwik.org/faq/how-to/#faq_62
|
|
227
203
|
"""
|
|
228
|
-
base_path, query_string = path.split(
|
|
229
|
-
infos = base_path.strip(
|
|
230
|
-
if
|
|
231
|
-
|
|
204
|
+
base_path, query_string = path.split("?")
|
|
205
|
+
infos = base_path.strip("/").split("/")[2:] # Removes api/version.
|
|
206
|
+
if (
|
|
207
|
+
base_path == "/api/1/" or base_path == "/api/2/"
|
|
208
|
+
): # The API root endpoint redirects to swagger doc.
|
|
209
|
+
return safe_unicode("apidoc")
|
|
232
210
|
if len(infos) > 1: # This is an object.
|
|
233
|
-
name =
|
|
234
|
-
category=infos[0].title(),
|
|
235
|
-
name=infos[1].replace('-', ' ').title()
|
|
211
|
+
name = "{category} / {name}".format(
|
|
212
|
+
category=infos[0].title(), name=infos[1].replace("-", " ").title()
|
|
236
213
|
)
|
|
237
214
|
else: # This is a collection.
|
|
238
|
-
name =
|
|
215
|
+
name = "{category}".format(category=infos[0].title())
|
|
239
216
|
return safe_unicode(name)
|
|
240
217
|
|
|
241
218
|
|
|
@@ -243,71 +220,71 @@ def extract_name_from_path(path):
|
|
|
243
220
|
@apiv2_blueprint.after_request
|
|
244
221
|
def collect_stats(response):
|
|
245
222
|
action_name = extract_name_from_path(request.full_path)
|
|
246
|
-
blacklist = current_app.config.get(
|
|
247
|
-
if
|
|
248
|
-
request.endpoint not in blacklist):
|
|
223
|
+
blacklist = current_app.config.get("TRACKING_BLACKLIST", [])
|
|
224
|
+
if not current_app.config["TESTING"] and request.endpoint not in blacklist:
|
|
249
225
|
extras = {
|
|
250
|
-
|
|
226
|
+
"action_name": urllib.parse.quote(action_name),
|
|
251
227
|
}
|
|
252
228
|
tracking.send_signal(on_api_call, request, current_user, **extras)
|
|
253
229
|
return response
|
|
254
230
|
|
|
255
231
|
|
|
256
|
-
default_error = api.model(
|
|
257
|
-
'message': fields.String
|
|
258
|
-
})
|
|
232
|
+
default_error = api.model("Error", {"message": fields.String})
|
|
259
233
|
|
|
260
234
|
|
|
261
235
|
@api.errorhandler(PermissionDenied)
|
|
262
236
|
@api.marshal_with(default_error, code=403)
|
|
263
237
|
def handle_permission_denied(error):
|
|
264
|
-
|
|
265
|
-
message =
|
|
266
|
-
return {
|
|
238
|
+
"""Error occuring when the user does not have the required permissions"""
|
|
239
|
+
message = "You do not have the permission to modify that object."
|
|
240
|
+
return {"message": message}, 403
|
|
267
241
|
|
|
268
242
|
|
|
269
243
|
@api.errorhandler(ValueError)
|
|
270
244
|
@api.marshal_with(default_error, code=400)
|
|
271
245
|
def handle_value_error(error):
|
|
272
|
-
|
|
273
|
-
return {
|
|
246
|
+
"""A generic value error"""
|
|
247
|
+
return {"message": str(error)}, 400
|
|
274
248
|
|
|
275
249
|
|
|
276
250
|
@api.errorhandler(UnauthorizedFileType)
|
|
277
251
|
@api.marshal_with(default_error, code=400)
|
|
278
252
|
def handle_unauthorized_file_type(error):
|
|
279
|
-
|
|
280
|
-
url = url_for(
|
|
253
|
+
"""Error occuring when the user try to upload a non-allowed file type"""
|
|
254
|
+
url = url_for("api.allowed_extensions", _external=True)
|
|
281
255
|
msg = (
|
|
282
|
-
|
|
283
|
-
'The allowed file type list is available at {url}'
|
|
256
|
+
"This file type is not allowed." "The allowed file type list is available at {url}"
|
|
284
257
|
).format(url=url)
|
|
285
|
-
return {
|
|
258
|
+
return {"message": msg}, 400
|
|
259
|
+
|
|
286
260
|
|
|
261
|
+
validation_error_fields = api.model("ValidationError", {"errors": fields.Raw})
|
|
287
262
|
|
|
288
|
-
validation_error_fields = api.model('ValidationError', {
|
|
289
|
-
'errors': fields.Raw
|
|
290
|
-
})
|
|
291
263
|
|
|
292
264
|
@api.errorhandler(FieldValidationError)
|
|
293
265
|
@api.marshal_with(validation_error_fields, code=400)
|
|
294
266
|
def handle_validation_error(error: FieldValidationError):
|
|
295
|
-
|
|
267
|
+
"""A validation error"""
|
|
296
268
|
errors = {}
|
|
297
269
|
errors[error.field] = [error.message]
|
|
298
270
|
|
|
299
|
-
return {
|
|
271
|
+
return {"errors": errors}, 400
|
|
272
|
+
|
|
300
273
|
|
|
301
274
|
class API(Resource): # Avoid name collision as resource is a core model
|
|
302
275
|
pass
|
|
303
276
|
|
|
304
277
|
|
|
305
|
-
base_reference = api.model(
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
278
|
+
base_reference = api.model(
|
|
279
|
+
"BaseReference",
|
|
280
|
+
{
|
|
281
|
+
"id": fields.String(description="The object unique identifier", required=True),
|
|
282
|
+
"class": fields.ClassName(
|
|
283
|
+
description="The object class", discriminator=True, required=True
|
|
284
|
+
),
|
|
285
|
+
},
|
|
286
|
+
description="Base model for reference field, aka. inline model reference",
|
|
287
|
+
)
|
|
311
288
|
|
|
312
289
|
|
|
313
290
|
def marshal_page(page, page_fields):
|
|
@@ -340,14 +317,14 @@ def init_app(app):
|
|
|
340
317
|
import udata.core.topic.api # noqa
|
|
341
318
|
import udata.core.topic.apiv2 # noqa
|
|
342
319
|
import udata.core.post.api # noqa
|
|
343
|
-
import udata.core.contact_point.api
|
|
320
|
+
import udata.core.contact_point.api # noqa
|
|
344
321
|
import udata.features.transfer.api # noqa
|
|
345
322
|
import udata.features.notifications.api # noqa
|
|
346
323
|
import udata.features.identicon.api # noqa
|
|
347
324
|
import udata.features.territories.api # noqa
|
|
348
325
|
import udata.harvest.api # noqa
|
|
349
326
|
|
|
350
|
-
for module in entrypoints.get_enabled(
|
|
327
|
+
for module in entrypoints.get_enabled("udata.apis", app).values():
|
|
351
328
|
api_module = module if inspect.ismodule(module) else import_module(module)
|
|
352
329
|
|
|
353
330
|
# api.init_app(app)
|
|
@@ -355,5 +332,5 @@ def init_app(app):
|
|
|
355
332
|
app.register_blueprint(apiv2_blueprint)
|
|
356
333
|
|
|
357
334
|
from udata.api.oauth2 import init_app as oauth2_init_app
|
|
335
|
+
|
|
358
336
|
oauth2_init_app(app)
|
|
359
|
-
cors.init_app(app)
|
udata/api/commands.py
CHANGED
|
@@ -2,79 +2,87 @@ import logging
|
|
|
2
2
|
import os
|
|
3
3
|
|
|
4
4
|
import click
|
|
5
|
-
|
|
6
|
-
from werkzeug.security import gen_salt
|
|
7
|
-
from flask import json, current_app
|
|
5
|
+
from flask import current_app, json
|
|
8
6
|
from flask_restx import schemas
|
|
7
|
+
from werkzeug.security import gen_salt
|
|
9
8
|
|
|
10
9
|
from udata.api import api
|
|
11
|
-
from udata.commands import cli, success, exit_with_error
|
|
12
|
-
from udata.models import User
|
|
13
10
|
from udata.api.oauth2 import OAuth2Client
|
|
11
|
+
from udata.commands import cli, exit_with_error, success
|
|
12
|
+
from udata.models import User
|
|
14
13
|
|
|
15
14
|
log = logging.getLogger(__name__)
|
|
16
15
|
|
|
17
16
|
|
|
18
|
-
@cli.group(
|
|
17
|
+
@cli.group("api")
|
|
19
18
|
def grp():
|
|
20
|
-
|
|
19
|
+
"""API related operations"""
|
|
21
20
|
|
|
22
21
|
|
|
23
22
|
def json_to_file(data, filename, pretty=False):
|
|
24
|
-
|
|
23
|
+
"""Dump JSON data to a file"""
|
|
25
24
|
kwargs = dict(indent=4) if pretty else {}
|
|
26
25
|
dirname = os.path.dirname(filename)
|
|
27
26
|
if not os.path.exists(dirname):
|
|
28
27
|
os.makedirs(dirname)
|
|
29
28
|
dump = json.dumps(api.__schema__, **kwargs)
|
|
30
|
-
with open(filename,
|
|
31
|
-
f.write(dump.encode(
|
|
29
|
+
with open(filename, "wb") as f:
|
|
30
|
+
f.write(dump.encode("utf-8"))
|
|
32
31
|
|
|
33
32
|
|
|
34
33
|
@grp.command()
|
|
35
|
-
@click.argument(
|
|
36
|
-
@click.option(
|
|
34
|
+
@click.argument("filename")
|
|
35
|
+
@click.option("-p", "--pretty", is_flag=True, help="Pretty print")
|
|
37
36
|
def swagger(filename, pretty):
|
|
38
|
-
|
|
37
|
+
"""Dump the swagger specifications"""
|
|
39
38
|
json_to_file(api.__schema__, filename, pretty)
|
|
40
39
|
|
|
41
40
|
|
|
42
41
|
@grp.command()
|
|
43
|
-
@click.argument(
|
|
44
|
-
@click.option(
|
|
45
|
-
@click.option(
|
|
46
|
-
@click.option(
|
|
47
|
-
help='Export Swagger specifications')
|
|
42
|
+
@click.argument("filename")
|
|
43
|
+
@click.option("-p", "--pretty", is_flag=True, help="Pretty print")
|
|
44
|
+
@click.option("-u", "--urlvars", is_flag=True, help="Export query strings")
|
|
45
|
+
@click.option("-s", "--swagger", is_flag=True, help="Export Swagger specifications")
|
|
48
46
|
def postman(filename, pretty, urlvars, swagger):
|
|
49
|
-
|
|
47
|
+
"""Dump the API as a Postman collection"""
|
|
50
48
|
data = api.as_postman(urlvars=urlvars, swagger=swagger)
|
|
51
49
|
json_to_file(data, filename, pretty)
|
|
52
50
|
|
|
53
51
|
|
|
54
52
|
@grp.command()
|
|
55
53
|
def validate():
|
|
56
|
-
|
|
54
|
+
"""Validate the Swagger/OpenAPI specification with your config"""
|
|
57
55
|
with current_app.test_request_context():
|
|
58
56
|
schema = json.loads(json.dumps(api.__schema__))
|
|
59
57
|
try:
|
|
60
58
|
schemas.validate(schema)
|
|
61
|
-
success(
|
|
59
|
+
success("API specifications are valid")
|
|
62
60
|
except schemas.SchemaValidationError as e:
|
|
63
|
-
exit_with_error(
|
|
61
|
+
exit_with_error("API specifications are not valid", e)
|
|
64
62
|
|
|
65
63
|
|
|
66
64
|
@grp.command()
|
|
67
|
-
@click.option(
|
|
68
|
-
@click.option(
|
|
69
|
-
@click.option(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
@click.option(
|
|
65
|
+
@click.option("-n", "--client-name", default="client-01", help="Client's name")
|
|
66
|
+
@click.option("-u", "--user-email", help="User's email")
|
|
67
|
+
@click.option(
|
|
68
|
+
"--uri", multiple=True, default=["http://localhost:8080/login"], help="Client's redirect uri"
|
|
69
|
+
)
|
|
70
|
+
@click.option(
|
|
71
|
+
"-g",
|
|
72
|
+
"--grant-types",
|
|
73
|
+
multiple=True,
|
|
74
|
+
default=["authorization_code"],
|
|
75
|
+
help="Client's grant types",
|
|
76
|
+
)
|
|
77
|
+
@click.option("-s", "--scope", default="default", help="Client's scope")
|
|
78
|
+
@click.option(
|
|
79
|
+
"-r", "--response-types", multiple=True, default=["code"], help="Client's response types"
|
|
80
|
+
)
|
|
73
81
|
def create_oauth_client(client_name, user_email, uri, grant_types, scope, response_types):
|
|
74
|
-
|
|
82
|
+
"""Creates an OAuth2Client instance in DB"""
|
|
75
83
|
user = User.objects(email=user_email).first()
|
|
76
84
|
if user is None:
|
|
77
|
-
exit_with_error(
|
|
85
|
+
exit_with_error("No matching user to email")
|
|
78
86
|
|
|
79
87
|
client = OAuth2Client.objects.create(
|
|
80
88
|
name=client_name,
|
|
@@ -82,12 +90,12 @@ def create_oauth_client(client_name, user_email, uri, grant_types, scope, respon
|
|
|
82
90
|
grant_types=grant_types,
|
|
83
91
|
scope=scope,
|
|
84
92
|
response_types=response_types,
|
|
85
|
-
redirect_uris=uri
|
|
93
|
+
redirect_uris=uri,
|
|
86
94
|
)
|
|
87
95
|
|
|
88
|
-
click.echo(f
|
|
89
|
-
click.echo(f
|
|
90
|
-
click.echo(f
|
|
91
|
-
click.echo(f
|
|
92
|
-
click.echo(f
|
|
93
|
-
click.echo(f
|
|
96
|
+
click.echo(f"New OAuth client: {client.name}")
|
|
97
|
+
click.echo(f"Client's ID {client.id}")
|
|
98
|
+
click.echo(f"Client's secret {client.secret}")
|
|
99
|
+
click.echo(f"Client's grant_types {client.grant_types}")
|
|
100
|
+
click.echo(f"Client's response_types {client.response_types}")
|
|
101
|
+
click.echo(f"Client's URI {client.redirect_uris}")
|
udata/api/errors.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from udata.i18n import gettext as _
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
VALIDATION_ERROR = _(
|
|
4
|
+
"Validation error: your data cannot be updated for "
|
|
5
|
+
"now, we have been notified of the error and we will "
|
|
6
|
+
"fix it as soon as possible."
|
|
7
|
+
)
|