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/fields.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import logging
|
|
2
1
|
import datetime
|
|
2
|
+
import logging
|
|
3
3
|
|
|
4
4
|
import pytz
|
|
5
5
|
from dateutil.parser import parse
|
|
@@ -13,51 +13,57 @@ log = logging.getLogger(__name__)
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class ISODateTime(String):
|
|
16
|
-
__schema_format__ =
|
|
16
|
+
__schema_format__ = "date-time"
|
|
17
17
|
|
|
18
18
|
def format(self, value):
|
|
19
19
|
if isinstance(value, str):
|
|
20
20
|
value = parse(value)
|
|
21
|
-
if
|
|
21
|
+
if (
|
|
22
|
+
isinstance(value, datetime.date)
|
|
23
|
+
and not isinstance(value, datetime.datetime)
|
|
24
|
+
or value.tzinfo
|
|
25
|
+
):
|
|
22
26
|
return value.isoformat()
|
|
23
27
|
return pytz.utc.localize(value).isoformat()
|
|
24
28
|
|
|
25
29
|
|
|
26
30
|
class Markdown(String):
|
|
27
|
-
__schema_format__ =
|
|
31
|
+
__schema_format__ = "markdown"
|
|
28
32
|
|
|
29
33
|
|
|
30
34
|
class UrlFor(String):
|
|
31
35
|
def __init__(self, endpoint, mapper=None, **kwargs):
|
|
32
36
|
super(UrlFor, self).__init__(**kwargs)
|
|
33
37
|
self.endpoint = endpoint
|
|
34
|
-
self.fallback_endpoint = kwargs.pop(
|
|
38
|
+
self.fallback_endpoint = kwargs.pop("fallback_endpoint", None)
|
|
35
39
|
self.mapper = mapper or self.default_mapper
|
|
36
40
|
|
|
37
41
|
def default_mapper(self, obj):
|
|
38
|
-
return {
|
|
42
|
+
return {"id": str(obj.id)}
|
|
39
43
|
|
|
40
44
|
def output(self, key, obj, **kwargs):
|
|
41
|
-
return endpoint_for(
|
|
45
|
+
return endpoint_for(
|
|
46
|
+
self.endpoint, self.fallback_endpoint, _external=True, **self.mapper(obj)
|
|
47
|
+
)
|
|
42
48
|
|
|
43
49
|
|
|
44
50
|
class NextPageUrl(String):
|
|
45
51
|
def output(self, key, obj, **kwargs):
|
|
46
|
-
if not getattr(obj,
|
|
52
|
+
if not getattr(obj, "has_next", None):
|
|
47
53
|
return None
|
|
48
54
|
args = multi_to_dict(request.args)
|
|
49
55
|
args.update(request.view_args)
|
|
50
|
-
args[
|
|
56
|
+
args["page"] = obj.page + 1
|
|
51
57
|
return url_for(request.endpoint, _external=True, **args)
|
|
52
58
|
|
|
53
59
|
|
|
54
60
|
class PreviousPageUrl(String):
|
|
55
61
|
def output(self, key, obj, **kwargs):
|
|
56
|
-
if not getattr(obj,
|
|
62
|
+
if not getattr(obj, "has_prev", None):
|
|
57
63
|
return None
|
|
58
64
|
args = multi_to_dict(request.args)
|
|
59
65
|
args.update(request.view_args)
|
|
60
|
-
args[
|
|
66
|
+
args["page"] = obj.page - 1
|
|
61
67
|
return url_for(request.endpoint, _external=True, **args)
|
|
62
68
|
|
|
63
69
|
|
|
@@ -81,15 +87,11 @@ class ImageField(String):
|
|
|
81
87
|
|
|
82
88
|
def pager(page_fields):
|
|
83
89
|
pager_fields = {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
required=True, min=0),
|
|
91
|
-
'next_page': NextPageUrl(description='The next page URL if exists'),
|
|
92
|
-
'previous_page': PreviousPageUrl(
|
|
93
|
-
description='The previous page URL if exists')
|
|
90
|
+
"data": List(Nested(page_fields), attribute="objects", description="The page data"),
|
|
91
|
+
"page": Integer(description="The current page", required=True, min=1),
|
|
92
|
+
"page_size": Integer(description="The page size used for pagination", required=True, min=0),
|
|
93
|
+
"total": Integer(description="The total paginated items", required=True, min=0),
|
|
94
|
+
"next_page": NextPageUrl(description="The next page URL if exists"),
|
|
95
|
+
"previous_page": PreviousPageUrl(description="The previous page URL if exists"),
|
|
94
96
|
}
|
|
95
97
|
return pager_fields
|
udata/api/oauth2.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
2
|
OAuth 2 serveur implementation based on Authlib.
|
|
3
3
|
|
|
4
4
|
See the documentatiosn here:
|
|
@@ -12,22 +12,23 @@ Authlib provides SQLAlchemny mixins which help understanding:
|
|
|
12
12
|
|
|
13
13
|
As well as a sample application:
|
|
14
14
|
- https://github.com/authlib/example-oauth2-server
|
|
15
|
-
|
|
16
|
-
import fnmatch
|
|
17
|
-
|
|
18
|
-
from bson import ObjectId
|
|
15
|
+
"""
|
|
19
16
|
|
|
17
|
+
import fnmatch
|
|
20
18
|
from datetime import datetime, timedelta
|
|
21
19
|
|
|
22
|
-
from authlib.integrations.flask_oauth2.errors import _HTTPException as AuthlibFlaskException
|
|
23
20
|
from authlib.integrations.flask_oauth2 import AuthorizationServer, ResourceProtector
|
|
24
|
-
from authlib.
|
|
21
|
+
from authlib.integrations.flask_oauth2.errors import (
|
|
22
|
+
_HTTPException as AuthlibFlaskException,
|
|
23
|
+
)
|
|
24
|
+
from authlib.oauth2 import OAuth2Error
|
|
25
|
+
from authlib.oauth2.rfc6749 import ClientMixin, grants
|
|
26
|
+
from authlib.oauth2.rfc6749.util import list_to_scope, scope_to_list
|
|
25
27
|
from authlib.oauth2.rfc6750 import BearerTokenValidator
|
|
26
28
|
from authlib.oauth2.rfc7009 import RevocationEndpoint
|
|
27
29
|
from authlib.oauth2.rfc7636 import CodeChallenge
|
|
28
|
-
from
|
|
29
|
-
from
|
|
30
|
-
from flask import request, render_template, current_app
|
|
30
|
+
from bson import ObjectId
|
|
31
|
+
from flask import current_app, render_template, request
|
|
31
32
|
from flask_security.utils import verify_password
|
|
32
33
|
from werkzeug.exceptions import Unauthorized
|
|
33
34
|
from werkzeug.security import gen_salt
|
|
@@ -35,12 +36,12 @@ from werkzeug.security import gen_salt
|
|
|
35
36
|
from udata.app import csrf
|
|
36
37
|
from udata.auth import current_user, login_required, login_user
|
|
37
38
|
from udata.core.organization.models import Organization
|
|
38
|
-
from udata.
|
|
39
|
+
from udata.core.storages import default_image_basename, images
|
|
40
|
+
from udata.i18n import I18nBlueprint
|
|
41
|
+
from udata.i18n import lazy_gettext as _
|
|
39
42
|
from udata.mongo import db
|
|
40
|
-
from udata.core.storages import images, default_image_basename
|
|
41
|
-
|
|
42
43
|
|
|
43
|
-
blueprint = I18nBlueprint(
|
|
44
|
+
blueprint = I18nBlueprint("oauth", __name__, url_prefix="/oauth")
|
|
44
45
|
oauth = AuthorizationServer()
|
|
45
46
|
require_oauth = ResourceProtector()
|
|
46
47
|
|
|
@@ -51,13 +52,10 @@ REFRESH_EXPIRATION = 30 # days
|
|
|
51
52
|
EPOCH = datetime.fromtimestamp(0)
|
|
52
53
|
|
|
53
54
|
TOKEN_TYPES = {
|
|
54
|
-
|
|
55
|
+
"Bearer": _("Bearer Token"),
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
SCOPES = {
|
|
58
|
-
'default': _('Default scope'),
|
|
59
|
-
'admin': _('System administrator rights')
|
|
60
|
-
}
|
|
58
|
+
SCOPES = {"default": _("Default scope"), "admin": _("System administrator rights")}
|
|
61
59
|
|
|
62
60
|
|
|
63
61
|
class OAuth2Client(ClientMixin, db.Datetimed, db.Document):
|
|
@@ -66,22 +64,19 @@ class OAuth2Client(ClientMixin, db.Datetimed, db.Document):
|
|
|
66
64
|
name = db.StringField(required=True)
|
|
67
65
|
description = db.StringField()
|
|
68
66
|
|
|
69
|
-
owner = db.ReferenceField(
|
|
67
|
+
owner = db.ReferenceField("User")
|
|
70
68
|
organization = db.ReferenceField(Organization, reverse_delete_rule=db.NULLIFY)
|
|
71
|
-
image = db.ImageField(fs=images, basename=default_image_basename,
|
|
72
|
-
thumbnails=[150, 25])
|
|
69
|
+
image = db.ImageField(fs=images, basename=default_image_basename, thumbnails=[150, 25])
|
|
73
70
|
|
|
74
71
|
redirect_uris = db.ListField(db.StringField())
|
|
75
|
-
scope = db.StringField(default=
|
|
72
|
+
scope = db.StringField(default="default")
|
|
76
73
|
grant_types = db.ListField(db.StringField())
|
|
77
74
|
response_types = db.ListField(db.StringField())
|
|
78
75
|
|
|
79
76
|
confidential = db.BooleanField(default=False)
|
|
80
77
|
internal = db.BooleanField(default=False)
|
|
81
78
|
|
|
82
|
-
meta = {
|
|
83
|
-
'collection': 'oauth2_client'
|
|
84
|
-
}
|
|
79
|
+
meta = {"collection": "oauth2_client"}
|
|
85
80
|
|
|
86
81
|
def get_client_id(self):
|
|
87
82
|
return str(self.id)
|
|
@@ -103,15 +98,13 @@ class OAuth2Client(ClientMixin, db.Datetimed, db.Document):
|
|
|
103
98
|
|
|
104
99
|
def get_allowed_scope(self, scope):
|
|
105
100
|
if not scope:
|
|
106
|
-
return
|
|
101
|
+
return ""
|
|
107
102
|
allowed = set(scope_to_list(self.scope))
|
|
108
103
|
return list_to_scope([s for s in scope.split() if s in allowed])
|
|
109
104
|
|
|
110
105
|
def check_redirect_uri(self, redirect_uri):
|
|
111
106
|
if current_app.config.get("OAUTH2_ALLOW_WILDCARD_IN_REDIRECT_URI"):
|
|
112
|
-
return any(
|
|
113
|
-
fnmatch.fnmatch(redirect_uri, pattern) for pattern in self.redirect_uris
|
|
114
|
-
)
|
|
107
|
+
return any(fnmatch.fnmatch(redirect_uri, pattern) for pattern in self.redirect_uris)
|
|
115
108
|
else:
|
|
116
109
|
return redirect_uri in self.redirect_uris
|
|
117
110
|
|
|
@@ -120,8 +113,8 @@ class OAuth2Client(ClientMixin, db.Datetimed, db.Document):
|
|
|
120
113
|
|
|
121
114
|
def check_token_endpoint_auth_method(self, method):
|
|
122
115
|
if not self.has_client_secret():
|
|
123
|
-
return method ==
|
|
124
|
-
return method in (
|
|
116
|
+
return method == "none"
|
|
117
|
+
return method in ("client_secret_post", "client_secret_basic")
|
|
125
118
|
|
|
126
119
|
def check_response_type(self, response_type):
|
|
127
120
|
return True
|
|
@@ -138,25 +131,23 @@ class OAuth2Client(ClientMixin, db.Datetimed, db.Document):
|
|
|
138
131
|
|
|
139
132
|
|
|
140
133
|
class OAuth2Token(db.Document):
|
|
141
|
-
client = db.ReferenceField(
|
|
142
|
-
user = db.ReferenceField(
|
|
134
|
+
client = db.ReferenceField("OAuth2Client", required=True)
|
|
135
|
+
user = db.ReferenceField("User")
|
|
143
136
|
|
|
144
137
|
# currently only bearer is supported
|
|
145
|
-
token_type = db.StringField(choices=list(TOKEN_TYPES), default=
|
|
138
|
+
token_type = db.StringField(choices=list(TOKEN_TYPES), default="Bearer")
|
|
146
139
|
|
|
147
140
|
access_token = db.StringField(unique=True)
|
|
148
141
|
refresh_token = db.StringField(unique=True, sparse=True)
|
|
149
142
|
created_at = db.DateTimeField(default=datetime.utcnow, required=True)
|
|
150
143
|
expires_in = db.IntField(required=True, default=TOKEN_EXPIRATION)
|
|
151
|
-
scope = db.StringField(default=
|
|
144
|
+
scope = db.StringField(default="")
|
|
152
145
|
revoked = db.BooleanField(default=False)
|
|
153
146
|
|
|
154
|
-
meta = {
|
|
155
|
-
'collection': 'oauth2_token'
|
|
156
|
-
}
|
|
147
|
+
meta = {"collection": "oauth2_token"}
|
|
157
148
|
|
|
158
149
|
def __str__(self):
|
|
159
|
-
return
|
|
150
|
+
return "<OAuth2Token({0.client.name})>".format(self)
|
|
160
151
|
|
|
161
152
|
def get_scope(self):
|
|
162
153
|
return self.scope
|
|
@@ -179,24 +170,22 @@ class OAuth2Token(db.Document):
|
|
|
179
170
|
|
|
180
171
|
|
|
181
172
|
class OAuth2Code(db.Document):
|
|
182
|
-
user = db.ReferenceField(
|
|
183
|
-
client = db.ReferenceField(
|
|
173
|
+
user = db.ReferenceField("User", required=True)
|
|
174
|
+
client = db.ReferenceField("OAuth2Client", required=True)
|
|
184
175
|
|
|
185
176
|
code = db.StringField(required=True)
|
|
186
177
|
|
|
187
178
|
redirect_uri = db.StringField()
|
|
188
179
|
expires = db.DateTimeField()
|
|
189
180
|
|
|
190
|
-
scope = db.StringField(default=
|
|
181
|
+
scope = db.StringField(default="")
|
|
191
182
|
code_challenge = db.StringField()
|
|
192
183
|
code_challenge_method = db.StringField()
|
|
193
184
|
|
|
194
|
-
meta = {
|
|
195
|
-
'collection': 'oauth2_code'
|
|
196
|
-
}
|
|
185
|
+
meta = {"collection": "oauth2_code"}
|
|
197
186
|
|
|
198
187
|
def __str__(self):
|
|
199
|
-
return
|
|
188
|
+
return "<OAuth2Code({0.client.name}, {0.user.fullname})>".format(self)
|
|
200
189
|
|
|
201
190
|
def is_expired(self):
|
|
202
191
|
return self.expires < datetime.utcnow()
|
|
@@ -209,14 +198,11 @@ class OAuth2Code(db.Document):
|
|
|
209
198
|
|
|
210
199
|
|
|
211
200
|
class AuthorizationCodeGrant(grants.AuthorizationCodeGrant):
|
|
212
|
-
TOKEN_ENDPOINT_AUTH_METHODS = [
|
|
213
|
-
'client_secret_basic',
|
|
214
|
-
'client_secret_post'
|
|
215
|
-
]
|
|
201
|
+
TOKEN_ENDPOINT_AUTH_METHODS = ["client_secret_basic", "client_secret_post"]
|
|
216
202
|
|
|
217
203
|
def save_authorization_code(self, code, request):
|
|
218
|
-
code_challenge = request.data.get(
|
|
219
|
-
code_challenge_method = request.data.get(
|
|
204
|
+
code_challenge = request.data.get("code_challenge")
|
|
205
|
+
code_challenge_method = request.data.get("code_challenge_method")
|
|
220
206
|
expires = datetime.utcnow() + timedelta(seconds=GRANT_EXPIRATION)
|
|
221
207
|
auth_code = OAuth2Code.objects.create(
|
|
222
208
|
code=code,
|
|
@@ -268,9 +254,9 @@ class RefreshTokenGrant(grants.RefreshTokenGrant):
|
|
|
268
254
|
class RevokeToken(RevocationEndpoint):
|
|
269
255
|
def query_token(self, token, token_type_hint, client):
|
|
270
256
|
qs = OAuth2Token.objects(client=client)
|
|
271
|
-
if token_type_hint ==
|
|
257
|
+
if token_type_hint == "access_token":
|
|
272
258
|
return qs.filter(access_token=token).first()
|
|
273
|
-
elif token_type_hint ==
|
|
259
|
+
elif token_type_hint == "refresh_token":
|
|
274
260
|
return qs.filter(refresh_token=token).first()
|
|
275
261
|
else:
|
|
276
262
|
qs = qs(db.Q(access_token=token) | db.Q(refresh_token=token))
|
|
@@ -292,22 +278,22 @@ class BearerToken(BearerTokenValidator):
|
|
|
292
278
|
return token.revoked
|
|
293
279
|
|
|
294
280
|
|
|
295
|
-
@blueprint.route(
|
|
281
|
+
@blueprint.route("/token", methods=["POST"], localize=False, endpoint="token")
|
|
296
282
|
@csrf.exempt
|
|
297
283
|
def access_token():
|
|
298
284
|
return oauth.create_token_response()
|
|
299
285
|
|
|
300
286
|
|
|
301
|
-
@blueprint.route(
|
|
287
|
+
@blueprint.route("/revoke", methods=["POST"], localize=False)
|
|
302
288
|
@csrf.exempt
|
|
303
289
|
def revoke_token():
|
|
304
290
|
return oauth.create_endpoint_response(RevokeToken.ENDPOINT_NAME)
|
|
305
291
|
|
|
306
292
|
|
|
307
|
-
@blueprint.route(
|
|
293
|
+
@blueprint.route("/authorize", methods=["GET", "POST"])
|
|
308
294
|
@login_required
|
|
309
295
|
def authorize(*args, **kwargs):
|
|
310
|
-
if request.method ==
|
|
296
|
+
if request.method == "GET":
|
|
311
297
|
try:
|
|
312
298
|
grant = oauth.validate_consent_request(end_user=current_user)
|
|
313
299
|
except OAuth2Error as error:
|
|
@@ -315,10 +301,10 @@ def authorize(*args, **kwargs):
|
|
|
315
301
|
# Bypass authorization screen for internal clients
|
|
316
302
|
if grant.client.internal:
|
|
317
303
|
return oauth.create_authorization_response(grant_user=current_user)
|
|
318
|
-
return render_template(
|
|
319
|
-
elif request.method ==
|
|
320
|
-
accept =
|
|
321
|
-
decline =
|
|
304
|
+
return render_template("api/oauth_authorize.html", grant=grant)
|
|
305
|
+
elif request.method == "POST":
|
|
306
|
+
accept = "accept" in request.form
|
|
307
|
+
decline = "decline" in request.form
|
|
322
308
|
if accept and not decline:
|
|
323
309
|
grant_user = current_user
|
|
324
310
|
else:
|
|
@@ -326,30 +312,25 @@ def authorize(*args, **kwargs):
|
|
|
326
312
|
return oauth.create_authorization_response(grant_user=grant_user)
|
|
327
313
|
|
|
328
314
|
|
|
329
|
-
@blueprint.route(
|
|
315
|
+
@blueprint.route("/error")
|
|
330
316
|
def oauth_error():
|
|
331
|
-
return render_template(
|
|
317
|
+
return render_template("api/oauth_error.html")
|
|
332
318
|
|
|
333
319
|
|
|
334
320
|
def query_client(client_id):
|
|
335
|
-
|
|
321
|
+
"""Fetch client by ID"""
|
|
336
322
|
return OAuth2Client.objects(id=ObjectId(client_id)).first()
|
|
337
323
|
|
|
338
324
|
|
|
339
325
|
def save_token(token, request):
|
|
340
|
-
scope = token.pop(
|
|
341
|
-
if request.grant_type ==
|
|
326
|
+
scope = token.pop("scope", "")
|
|
327
|
+
if request.grant_type == "refresh_token":
|
|
342
328
|
credential = request.credential
|
|
343
329
|
credential.update(scope=scope, **token)
|
|
344
330
|
else:
|
|
345
331
|
client = request.client
|
|
346
332
|
user = request.user or client.owner
|
|
347
|
-
OAuth2Token.objects.create(
|
|
348
|
-
client=client,
|
|
349
|
-
user=user.id,
|
|
350
|
-
scope=scope,
|
|
351
|
-
**token
|
|
352
|
-
)
|
|
333
|
+
OAuth2Token.objects.create(client=client, user=user.id, scope=scope, **token)
|
|
353
334
|
|
|
354
335
|
|
|
355
336
|
def check_credentials():
|
udata/api/parsers.py
CHANGED
|
@@ -9,28 +9,28 @@ class ModelApiParser:
|
|
|
9
9
|
def __init__(self, paginate=True):
|
|
10
10
|
self.parser = api.parser()
|
|
11
11
|
# q parameter
|
|
12
|
-
self.parser.add_argument(
|
|
13
|
-
help='The search query')
|
|
12
|
+
self.parser.add_argument("q", type=str, location="args", help="The search query")
|
|
14
13
|
# Sort arguments
|
|
15
14
|
keys = list(self.sorts)
|
|
16
|
-
choices = keys + [
|
|
17
|
-
help_msg =
|
|
18
|
-
self.parser.add_argument(
|
|
19
|
-
choices=choices, help=help_msg)
|
|
15
|
+
choices = keys + ["-" + k for k in keys]
|
|
16
|
+
help_msg = "The field (and direction) on which sorting apply"
|
|
17
|
+
self.parser.add_argument("sort", type=str, location="args", choices=choices, help=help_msg)
|
|
20
18
|
if paginate:
|
|
21
|
-
self.parser.add_argument(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
self.parser.add_argument(
|
|
20
|
+
"page", type=int, location="args", default=1, help="The page to display"
|
|
21
|
+
)
|
|
22
|
+
self.parser.add_argument(
|
|
23
|
+
"page_size", type=int, location="args", default=20, help="The page size"
|
|
24
|
+
)
|
|
25
25
|
|
|
26
26
|
def parse(self):
|
|
27
27
|
args = self.parser.parse_args()
|
|
28
|
-
if args[
|
|
29
|
-
if args[
|
|
28
|
+
if args["sort"]:
|
|
29
|
+
if args["sort"].startswith("-"):
|
|
30
30
|
# Keyerror because of the '-' character in front of the argument.
|
|
31
31
|
# It is removed to find the value in dict and added back.
|
|
32
|
-
arg_sort = args[
|
|
33
|
-
args[
|
|
32
|
+
arg_sort = args["sort"][1:]
|
|
33
|
+
args["sort"] = "-" + self.sorts[arg_sort]
|
|
34
34
|
else:
|
|
35
|
-
args[
|
|
35
|
+
args["sort"] = self.sorts[args["sort"]]
|
|
36
36
|
return args
|
udata/api/signals.py
CHANGED