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/assets.py
CHANGED
|
@@ -3,16 +3,16 @@ from flask_cdn import url_for as cdn_url_for
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
def cdn_for(endpoint, **kwargs):
|
|
6
|
-
|
|
6
|
+
"""
|
|
7
7
|
Get a CDN URL for a static assets.
|
|
8
8
|
|
|
9
9
|
Do not use a replacement for all flask.url_for calls
|
|
10
10
|
as it is only meant for CDN assets URLS.
|
|
11
11
|
(There is some extra round trip which cost is justified
|
|
12
12
|
by the CDN assets prformance improvements)
|
|
13
|
-
|
|
14
|
-
if current_app.config[
|
|
15
|
-
if not current_app.config.get(
|
|
16
|
-
kwargs.pop(
|
|
13
|
+
"""
|
|
14
|
+
if current_app.config["CDN_DOMAIN"]:
|
|
15
|
+
if not current_app.config.get("CDN_DEBUG"):
|
|
16
|
+
kwargs.pop("_external", None) # Avoid the _external parameter in URL
|
|
17
17
|
return cdn_url_for(endpoint, **kwargs)
|
|
18
18
|
return url_for(endpoint, **kwargs)
|
udata/auth/__init__.py
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
|
-
from flask import current_app
|
|
4
|
-
from flask import render_template
|
|
5
|
-
|
|
6
|
-
from flask_principal import identity_loaded # noqa: facade pattern
|
|
3
|
+
from flask import current_app, render_template
|
|
7
4
|
from flask_principal import Permission as BasePermission
|
|
8
|
-
from flask_principal import
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
5
|
+
from flask_principal import (
|
|
6
|
+
PermissionDenied, # noqa: facade pattern
|
|
7
|
+
RoleNeed,
|
|
8
|
+
UserNeed, # noqa: facade pattern
|
|
9
|
+
identity_loaded, # noqa: facade pattern
|
|
10
|
+
)
|
|
11
|
+
from flask_security import ( # noqa
|
|
12
|
+
Security, # noqa
|
|
13
|
+
current_user,
|
|
14
|
+
login_required,
|
|
15
|
+
login_user,
|
|
14
16
|
)
|
|
15
17
|
from werkzeug.utils import import_string
|
|
16
18
|
|
|
@@ -19,7 +21,7 @@ log = logging.getLogger(__name__)
|
|
|
19
21
|
|
|
20
22
|
def render_security_template(*args, **kwargs):
|
|
21
23
|
try:
|
|
22
|
-
render = import_string(current_app.config.get(
|
|
24
|
+
render = import_string(current_app.config.get("SECURITY_RENDER"))
|
|
23
25
|
except Exception:
|
|
24
26
|
render = render_template
|
|
25
27
|
return render(*args, **kwargs)
|
|
@@ -30,29 +32,38 @@ security = Security()
|
|
|
30
32
|
|
|
31
33
|
class Permission(BasePermission):
|
|
32
34
|
def __init__(self, *needs):
|
|
33
|
-
|
|
34
|
-
super(Permission, self).__init__(RoleNeed(
|
|
35
|
+
"""Let administrator bypass all permissions"""
|
|
36
|
+
super(Permission, self).__init__(RoleNeed("admin"), *needs)
|
|
35
37
|
|
|
36
38
|
|
|
37
39
|
admin_permission = Permission()
|
|
38
40
|
|
|
39
41
|
|
|
40
42
|
def init_app(app):
|
|
41
|
-
from .
|
|
43
|
+
from udata.models import datastore
|
|
44
|
+
|
|
45
|
+
from .forms import (
|
|
46
|
+
ExtendedLoginForm,
|
|
47
|
+
ExtendedRegisterForm,
|
|
48
|
+
ExtendedResetPasswordForm,
|
|
49
|
+
)
|
|
42
50
|
from .mails import UdataMailUtil
|
|
43
51
|
from .password_validation import UdataPasswordUtil
|
|
44
52
|
from .views import create_security_blueprint
|
|
45
|
-
|
|
46
|
-
security.init_app(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
|
|
54
|
+
security.init_app(
|
|
55
|
+
app,
|
|
56
|
+
datastore,
|
|
57
|
+
register_blueprint=False,
|
|
58
|
+
render_template=render_security_template,
|
|
59
|
+
login_form=ExtendedLoginForm,
|
|
60
|
+
confirm_register_form=ExtendedRegisterForm,
|
|
61
|
+
register_form=ExtendedRegisterForm,
|
|
62
|
+
reset_password_form=ExtendedResetPasswordForm,
|
|
63
|
+
mail_util_cls=UdataMailUtil,
|
|
64
|
+
password_util_cls=UdataPasswordUtil,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
security_bp = create_security_blueprint(app, app.extensions["security"], "security_blueprint")
|
|
57
68
|
|
|
58
69
|
app.register_blueprint(security_bp)
|
udata/auth/forms.py
CHANGED
|
@@ -2,23 +2,31 @@ import datetime
|
|
|
2
2
|
|
|
3
3
|
from flask import current_app
|
|
4
4
|
from flask_login import current_user
|
|
5
|
-
from flask_security.forms import
|
|
6
|
-
|
|
7
|
-
from udata.forms import validators
|
|
5
|
+
from flask_security.forms import Form, LoginForm, RegisterForm, ResetPasswordForm
|
|
6
|
+
|
|
7
|
+
from udata.forms import fields, validators
|
|
8
8
|
from udata.i18n import lazy_gettext as _
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class ExtendedRegisterForm(RegisterForm):
|
|
12
12
|
first_name = fields.StringField(
|
|
13
|
-
_(
|
|
14
|
-
|
|
13
|
+
_("First name"),
|
|
14
|
+
[
|
|
15
|
+
validators.DataRequired(_("First name is required")),
|
|
16
|
+
validators.NoURLs(_("URLs not allowed in this field")),
|
|
17
|
+
],
|
|
18
|
+
)
|
|
15
19
|
last_name = fields.StringField(
|
|
16
|
-
_(
|
|
17
|
-
|
|
20
|
+
_("Last name"),
|
|
21
|
+
[
|
|
22
|
+
validators.DataRequired(_("Last name is required")),
|
|
23
|
+
validators.NoURLs(_("URLs not allowed in this field")),
|
|
24
|
+
],
|
|
25
|
+
)
|
|
18
26
|
|
|
19
27
|
def validate(self):
|
|
20
28
|
# no register allowed when read only mode is on
|
|
21
|
-
if not super().validate() or current_app.config.get(
|
|
29
|
+
if not super().validate() or current_app.config.get("READ_ONLY_MODE"):
|
|
22
30
|
return False
|
|
23
31
|
|
|
24
32
|
return True
|
|
@@ -30,7 +38,7 @@ class ExtendedLoginForm(LoginForm):
|
|
|
30
38
|
return False
|
|
31
39
|
|
|
32
40
|
if self.user.password_rotation_demanded:
|
|
33
|
-
self.password.errors.append(_(
|
|
41
|
+
self.password.errors.append(_("Password must be changed for security reasons"))
|
|
34
42
|
return False
|
|
35
43
|
|
|
36
44
|
return True
|
|
@@ -50,12 +58,12 @@ class ExtendedResetPasswordForm(ResetPasswordForm):
|
|
|
50
58
|
|
|
51
59
|
|
|
52
60
|
class ChangeEmailForm(Form):
|
|
53
|
-
new_email = fields.StringField(_(
|
|
61
|
+
new_email = fields.StringField(_("New email"), [validators.DataRequired(), validators.Email()])
|
|
54
62
|
new_email_confirm = fields.StringField(
|
|
55
|
-
_(
|
|
56
|
-
[validators.EqualTo(
|
|
63
|
+
_("Retype email"),
|
|
64
|
+
[validators.EqualTo("new_email", message=_("Email does not match")), validators.Email()],
|
|
57
65
|
)
|
|
58
|
-
submit = fields.SubmitField(_(
|
|
66
|
+
submit = fields.SubmitField(_("Change email"))
|
|
59
67
|
|
|
60
68
|
def validate(self):
|
|
61
69
|
if not super().validate():
|
|
@@ -65,7 +73,7 @@ class ChangeEmailForm(Form):
|
|
|
65
73
|
|
|
66
74
|
if self.user.email.strip() == self.new_email.data.strip():
|
|
67
75
|
self.new_email.errors.append(
|
|
68
|
-
|
|
69
|
-
|
|
76
|
+
"Your new email must be different than your " "previous email"
|
|
77
|
+
)
|
|
70
78
|
return False
|
|
71
79
|
return True
|
udata/auth/helpers.py
CHANGED
|
@@ -5,6 +5,6 @@ from flask_security import current_user
|
|
|
5
5
|
def current_user_is_admin_or_self() -> bool:
|
|
6
6
|
if current_user.is_anonymous:
|
|
7
7
|
return False
|
|
8
|
-
if request.endpoint ==
|
|
8
|
+
if request.endpoint == "api.me" or current_user.sysadmin:
|
|
9
9
|
return True
|
|
10
10
|
return False
|
udata/auth/mails.py
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import email_validator
|
|
2
2
|
from flask import current_app
|
|
3
|
+
|
|
3
4
|
from udata.tasks import task
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
@task
|
|
7
8
|
def sendmail(msg):
|
|
8
|
-
debug = current_app.config.get(
|
|
9
|
-
send_mail = current_app.config.get(
|
|
9
|
+
debug = current_app.config.get("DEBUG", False)
|
|
10
|
+
send_mail = current_app.config.get("SEND_MAIL", not debug)
|
|
10
11
|
if send_mail:
|
|
11
12
|
mail = current_app.extensions.get("mail")
|
|
12
13
|
mail.send(msg)
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class UdataMailUtil:
|
|
16
|
-
|
|
17
17
|
def __init__(self, app):
|
|
18
18
|
self.app = app
|
|
19
19
|
|
|
@@ -7,13 +7,12 @@ from udata.i18n import lazy_gettext as _
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class UdataPasswordUtil:
|
|
10
|
-
|
|
11
10
|
def __init__(self, app):
|
|
12
11
|
pass
|
|
13
12
|
|
|
14
13
|
@staticmethod
|
|
15
14
|
def normalize(password):
|
|
16
|
-
cf = current_app.config.get(
|
|
15
|
+
cf = current_app.config.get("SECURITY_PASSWORD_NORMALIZE_FORM")
|
|
17
16
|
if cf:
|
|
18
17
|
return unicodedata.normalize(cf, password)
|
|
19
18
|
return password
|
|
@@ -22,28 +21,33 @@ class UdataPasswordUtil:
|
|
|
22
21
|
pnorm = self.normalize(password)
|
|
23
22
|
|
|
24
23
|
error_list = []
|
|
25
|
-
pass_length = current_app.config.get(
|
|
24
|
+
pass_length = current_app.config.get("SECURITY_PASSWORD_LENGTH_MIN")
|
|
26
25
|
if len(pnorm) < pass_length:
|
|
27
|
-
message = _(
|
|
26
|
+
message = _("Password must be at least {pass_length} characters long")
|
|
28
27
|
error_list.append(message.format(pass_length=pass_length))
|
|
29
28
|
|
|
30
29
|
# searching for lowercase
|
|
31
|
-
if current_app.config.get(
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
if current_app.config.get("SECURITY_PASSWORD_REQUIREMENTS_LOWERCASE") and (
|
|
31
|
+
re.search(r"[a-z]", pnorm) is None
|
|
32
|
+
):
|
|
33
|
+
error_list.append(_("Password must contain lowercases"))
|
|
34
34
|
|
|
35
35
|
# searching for digits
|
|
36
|
-
if current_app.config.get(
|
|
37
|
-
|
|
36
|
+
if current_app.config.get("SECURITY_PASSWORD_REQUIREMENTS_DIGITS") and (
|
|
37
|
+
re.search(r"\d", pnorm) is None
|
|
38
|
+
):
|
|
39
|
+
error_list.append(_("Password must contain digits"))
|
|
38
40
|
|
|
39
41
|
# searching for uppercase
|
|
40
|
-
if current_app.config.get(
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
if current_app.config.get("SECURITY_PASSWORD_REQUIREMENTS_UPPERCASE") and (
|
|
43
|
+
re.search(r"[A-Z]", pnorm) is None
|
|
44
|
+
):
|
|
45
|
+
error_list.append(_("Password must contain uppercases"))
|
|
43
46
|
|
|
44
47
|
# searching for symbols
|
|
45
|
-
if current_app.config.get(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
if current_app.config.get("SECURITY_PASSWORD_REQUIREMENTS_SYMBOLS") and (
|
|
49
|
+
re.search(r"[ !#$%&'()*+,-./[\\\]^_`{|}~" + r'"]', pnorm) is None
|
|
50
|
+
):
|
|
51
|
+
error_list.append(_("Password must contain symbols"))
|
|
48
52
|
|
|
49
53
|
return error_list, pnorm
|
udata/auth/views.py
CHANGED
|
@@ -1,56 +1,67 @@
|
|
|
1
|
-
from flask import flash, redirect,
|
|
1
|
+
from flask import current_app, flash, redirect, url_for
|
|
2
2
|
from flask_login import current_user, login_required
|
|
3
|
-
from flask_security.views import change_password
|
|
4
|
-
from flask_security.views import confirm_email
|
|
5
|
-
from flask_security.views import forgot_password
|
|
6
|
-
from flask_security.views import login
|
|
7
|
-
from flask_security.views import logout
|
|
8
|
-
from flask_security.views import register
|
|
9
|
-
from flask_security.views import reset_password
|
|
10
|
-
from flask_security.views import send_confirmation
|
|
11
|
-
from flask_security.views import send_login
|
|
12
|
-
from flask_security.views import token_login
|
|
13
3
|
from flask_security.utils import (
|
|
14
|
-
check_and_get_token_status,
|
|
15
|
-
|
|
4
|
+
check_and_get_token_status,
|
|
5
|
+
do_flash,
|
|
6
|
+
get_message,
|
|
7
|
+
get_within_delta,
|
|
8
|
+
hash_data,
|
|
9
|
+
login_user,
|
|
10
|
+
logout_user,
|
|
11
|
+
send_mail,
|
|
12
|
+
verify_hash,
|
|
13
|
+
)
|
|
14
|
+
from flask_security.views import (
|
|
15
|
+
change_password,
|
|
16
|
+
confirm_email,
|
|
17
|
+
forgot_password,
|
|
18
|
+
login,
|
|
19
|
+
logout,
|
|
20
|
+
register,
|
|
21
|
+
reset_password,
|
|
22
|
+
send_confirmation,
|
|
23
|
+
send_login,
|
|
24
|
+
token_login,
|
|
25
|
+
)
|
|
16
26
|
from werkzeug.local import LocalProxy
|
|
27
|
+
|
|
17
28
|
from udata.i18n import lazy_gettext as _
|
|
18
29
|
from udata.uris import endpoint_for
|
|
19
30
|
|
|
20
31
|
from .forms import ChangeEmailForm
|
|
21
32
|
|
|
22
|
-
|
|
23
|
-
_security = LocalProxy(lambda: current_app.extensions['security'])
|
|
33
|
+
_security = LocalProxy(lambda: current_app.extensions["security"])
|
|
24
34
|
_datastore = LocalProxy(lambda: _security.datastore)
|
|
25
35
|
|
|
26
36
|
|
|
27
37
|
def slash_url_suffix(url, suffix):
|
|
28
38
|
"""Adds a slash either to the beginning or the end of a suffix
|
|
29
|
-
|
|
30
|
-
|
|
39
|
+
(which is to be appended to a URL), depending on whether or not
|
|
40
|
+
the URL ends with a slash.
|
|
31
41
|
"""
|
|
32
42
|
|
|
33
|
-
return url.endswith(
|
|
43
|
+
return url.endswith("/") and ("%s/" % suffix) or ("/%s" % suffix)
|
|
34
44
|
|
|
35
45
|
|
|
36
46
|
def send_change_email_confirmation_instructions(user, new_email):
|
|
37
47
|
data = [str(current_user.fs_uniquifier), hash_data(current_user.email), new_email]
|
|
38
48
|
token = _security.confirm_serializer.dumps(data)
|
|
39
|
-
confirmation_link = url_for(
|
|
49
|
+
confirmation_link = url_for("security.confirm_change_email", token=token, _external=True)
|
|
40
50
|
|
|
41
|
-
subject = _(
|
|
51
|
+
subject = _("Confirm change of email instructions")
|
|
42
52
|
send_mail(
|
|
43
53
|
subject=subject,
|
|
44
54
|
recipient=new_email,
|
|
45
|
-
template=
|
|
55
|
+
template="confirmation_instructions",
|
|
46
56
|
user=current_user,
|
|
47
|
-
confirmation_link=confirmation_link
|
|
57
|
+
confirmation_link=confirmation_link,
|
|
48
58
|
)
|
|
49
59
|
|
|
50
60
|
|
|
51
61
|
def confirm_change_email_token_status(token):
|
|
52
62
|
expired, invalid, token_data = check_and_get_token_status(
|
|
53
|
-
token,
|
|
63
|
+
token, "confirm", get_within_delta("CONFIRM_EMAIL_WITHIN")
|
|
64
|
+
)
|
|
54
65
|
new_email = None
|
|
55
66
|
|
|
56
67
|
if not invalid and token_data:
|
|
@@ -62,17 +73,21 @@ def confirm_change_email_token_status(token):
|
|
|
62
73
|
|
|
63
74
|
|
|
64
75
|
def confirm_change_email(token):
|
|
65
|
-
expired, invalid, user, new_email = (
|
|
66
|
-
confirm_change_email_token_status(token))
|
|
76
|
+
expired, invalid, user, new_email = confirm_change_email_token_status(token)
|
|
67
77
|
|
|
68
78
|
if not user or invalid:
|
|
69
79
|
invalid = True
|
|
70
|
-
do_flash(*get_message(
|
|
80
|
+
do_flash(*get_message("INVALID_CONFIRMATION_TOKEN"))
|
|
71
81
|
if expired:
|
|
72
82
|
send_change_email_confirmation_instructions(user, new_email)
|
|
73
|
-
do_flash(
|
|
83
|
+
do_flash(
|
|
84
|
+
_(
|
|
85
|
+
"You did not confirm your change of email within {email_within}. New instructions to confirm your change of email have been sent to {new_email}."
|
|
86
|
+
).format(email_within=_security.confirm_email_within, new_email=new_email),
|
|
87
|
+
"error",
|
|
88
|
+
)
|
|
74
89
|
if invalid or expired:
|
|
75
|
-
return redirect(endpoint_for(
|
|
90
|
+
return redirect(endpoint_for("site.home", "admin.index"))
|
|
76
91
|
|
|
77
92
|
if user != current_user:
|
|
78
93
|
logout_user()
|
|
@@ -80,10 +95,10 @@ def confirm_change_email(token):
|
|
|
80
95
|
|
|
81
96
|
user.email = new_email
|
|
82
97
|
_datastore.put(user)
|
|
83
|
-
msg = (_(
|
|
98
|
+
msg = (_("Thank you. Your change of email has been confirmed."), "success")
|
|
84
99
|
|
|
85
100
|
do_flash(*msg)
|
|
86
|
-
return redirect(endpoint_for(
|
|
101
|
+
return redirect(endpoint_for("site.home", "admin.index"))
|
|
87
102
|
|
|
88
103
|
|
|
89
104
|
@login_required
|
|
@@ -95,10 +110,15 @@ def change_email():
|
|
|
95
110
|
if form.validate_on_submit():
|
|
96
111
|
new_email = form.new_email.data
|
|
97
112
|
send_change_email_confirmation_instructions(current_user, new_email)
|
|
98
|
-
flash(
|
|
99
|
-
|
|
113
|
+
flash(
|
|
114
|
+
_(
|
|
115
|
+
"Thank you. Confirmation instructions for changing your email have been sent to {new_email}."
|
|
116
|
+
).format(new_email=new_email),
|
|
117
|
+
"success",
|
|
118
|
+
)
|
|
119
|
+
return redirect(endpoint_for("site.home", "admin.index"))
|
|
100
120
|
|
|
101
|
-
return _security.render_template(
|
|
121
|
+
return _security.render_template("security/change_email.html", change_email_form=form)
|
|
102
122
|
|
|
103
123
|
|
|
104
124
|
def create_security_blueprint(app, state, import_name):
|
|
@@ -108,60 +128,66 @@ def create_security_blueprint(app, state, import_name):
|
|
|
108
128
|
Creates the security extension blueprint
|
|
109
129
|
This creates an I18nBlueprint to use as a base.
|
|
110
130
|
"""
|
|
111
|
-
bp = I18nBlueprint(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
131
|
+
bp = I18nBlueprint(
|
|
132
|
+
state.blueprint_name,
|
|
133
|
+
import_name,
|
|
134
|
+
url_prefix=state.url_prefix,
|
|
135
|
+
subdomain=state.subdomain,
|
|
136
|
+
template_folder="templates",
|
|
137
|
+
)
|
|
115
138
|
|
|
116
|
-
bp.route(app.config[
|
|
139
|
+
bp.route(app.config["SECURITY_LOGOUT_URL"], endpoint="logout")(logout)
|
|
117
140
|
|
|
118
141
|
if state.passwordless:
|
|
119
|
-
bp.route(app.config[
|
|
120
|
-
|
|
121
|
-
|
|
142
|
+
bp.route(app.config["SECURITY_LOGIN_URL"], methods=["GET", "POST"], endpoint="login")(
|
|
143
|
+
send_login
|
|
144
|
+
)
|
|
122
145
|
bp.route(
|
|
123
|
-
app.config[
|
|
124
|
-
|
|
146
|
+
app.config["SECURITY_LOGIN_URL"]
|
|
147
|
+
+ slash_url_suffix(app.config["SECURITY_LOGIN_URL"], "<token>"),
|
|
148
|
+
endpoint="token_login",
|
|
125
149
|
)(token_login)
|
|
126
150
|
else:
|
|
127
|
-
bp.route(app.config[
|
|
128
|
-
methods=['GET', 'POST'],
|
|
129
|
-
endpoint='login')(login)
|
|
151
|
+
bp.route(app.config["SECURITY_LOGIN_URL"], methods=["GET", "POST"], endpoint="login")(login)
|
|
130
152
|
|
|
131
153
|
if state.registerable:
|
|
132
|
-
bp.route(app.config[
|
|
133
|
-
|
|
134
|
-
|
|
154
|
+
bp.route(app.config["SECURITY_REGISTER_URL"], methods=["GET", "POST"], endpoint="register")(
|
|
155
|
+
register
|
|
156
|
+
)
|
|
135
157
|
|
|
136
158
|
if state.recoverable:
|
|
137
|
-
bp.route(app.config['SECURITY_RESET_URL'],
|
|
138
|
-
methods=['GET', 'POST'],
|
|
139
|
-
endpoint='forgot_password')(forgot_password)
|
|
140
159
|
bp.route(
|
|
141
|
-
app.config[
|
|
142
|
-
|
|
143
|
-
|
|
160
|
+
app.config["SECURITY_RESET_URL"], methods=["GET", "POST"], endpoint="forgot_password"
|
|
161
|
+
)(forgot_password)
|
|
162
|
+
bp.route(
|
|
163
|
+
app.config["SECURITY_RESET_URL"]
|
|
164
|
+
+ slash_url_suffix(app.config["SECURITY_RESET_URL"], "<token>"),
|
|
165
|
+
methods=["GET", "POST"],
|
|
166
|
+
endpoint="reset_password",
|
|
144
167
|
)(reset_password)
|
|
145
168
|
|
|
146
169
|
if state.changeable:
|
|
147
|
-
bp.route(
|
|
148
|
-
|
|
149
|
-
|
|
170
|
+
bp.route(
|
|
171
|
+
app.config["SECURITY_CHANGE_URL"], methods=["GET", "POST"], endpoint="change_password"
|
|
172
|
+
)(change_password)
|
|
150
173
|
|
|
151
174
|
if state.confirmable:
|
|
152
|
-
bp.route(app.config['SECURITY_CONFIRM_URL'],
|
|
153
|
-
methods=['GET', 'POST'],
|
|
154
|
-
endpoint='send_confirmation')(send_confirmation)
|
|
155
175
|
bp.route(
|
|
156
|
-
app.config[
|
|
157
|
-
methods=[
|
|
158
|
-
endpoint=
|
|
176
|
+
app.config["SECURITY_CONFIRM_URL"],
|
|
177
|
+
methods=["GET", "POST"],
|
|
178
|
+
endpoint="send_confirmation",
|
|
179
|
+
)(send_confirmation)
|
|
180
|
+
bp.route(
|
|
181
|
+
app.config["SECURITY_CONFIRM_URL"]
|
|
182
|
+
+ slash_url_suffix(app.config["SECURITY_CONFIRM_URL"], "<token>"),
|
|
183
|
+
methods=["GET", "POST"],
|
|
184
|
+
endpoint="confirm_email",
|
|
159
185
|
)(confirm_email)
|
|
160
186
|
|
|
161
|
-
bp.route(
|
|
162
|
-
|
|
163
|
-
|
|
187
|
+
bp.route(
|
|
188
|
+
"/confirm-change-email/<token>", methods=["GET", "POST"], endpoint="confirm_change_email"
|
|
189
|
+
)(confirm_change_email)
|
|
164
190
|
|
|
165
|
-
bp.route(
|
|
191
|
+
bp.route("/change-email", methods=["GET", "POST"], endpoint="change_email")(change_email)
|
|
166
192
|
|
|
167
193
|
return bp
|