udata 9.1.2.dev30355__py2.py3-none-any.whl → 9.1.2.dev30382__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 +135 -124
- 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 +56 -54
- 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/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/static/chunks/{11.e9b9ca1f3e03d4020377.js → 11.52e531c19f8de80c00cf.js} +3 -3
- udata/static/chunks/{11.e9b9ca1f3e03d4020377.js.map → 11.52e531c19f8de80c00cf.js.map} +1 -1
- udata/static/chunks/{13.038c0d9aa0dfa0181c4b.js → 13.c3343a7f1070061c0e10.js} +2 -2
- udata/static/chunks/{13.038c0d9aa0dfa0181c4b.js.map → 13.c3343a7f1070061c0e10.js.map} +1 -1
- udata/static/chunks/{16.0baa2b64a74a2dcde25c.js → 16.8fa42440ad75ca172e6d.js} +2 -2
- udata/static/chunks/{16.0baa2b64a74a2dcde25c.js.map → 16.8fa42440ad75ca172e6d.js.map} +1 -1
- udata/static/chunks/{19.350a9f150b074b4ecefa.js → 19.9c6c8412729cd6d59cfa.js} +3 -3
- udata/static/chunks/{19.350a9f150b074b4ecefa.js.map → 19.9c6c8412729cd6d59cfa.js.map} +1 -1
- udata/static/chunks/{5.6ebbce2b9b3e696d3da5.js → 5.71d15c2e4f21feee2a9a.js} +3 -3
- udata/static/chunks/{5.6ebbce2b9b3e696d3da5.js.map → 5.71d15c2e4f21feee2a9a.js.map} +1 -1
- udata/static/chunks/{6.d8a5f7b017bcbd083641.js → 6.9139dc098b8ea640b890.js} +3 -3
- udata/static/chunks/{6.d8a5f7b017bcbd083641.js.map → 6.9139dc098b8ea640b890.js.map} +1 -1
- udata/static/common.js +1 -1
- udata/static/common.js.map +1 -1
- 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 +5 -6
- udata/tests/api/test_auth_api.py +395 -321
- udata/tests/api/test_base_api.py +31 -33
- 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 +76 -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_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.dev30382.dist-info}/METADATA +3 -2
- udata-9.1.2.dev30382.dist-info/RECORD +704 -0
- udata-9.1.2.dev30355.dist-info/RECORD +0 -704
- {udata-9.1.2.dev30355.dist-info → udata-9.1.2.dev30382.dist-info}/LICENSE +0 -0
- {udata-9.1.2.dev30355.dist-info → udata-9.1.2.dev30382.dist-info}/WHEEL +0 -0
- {udata-9.1.2.dev30355.dist-info → udata-9.1.2.dev30382.dist-info}/entry_points.txt +0 -0
- {udata-9.1.2.dev30355.dist-info → udata-9.1.2.dev30382.dist-info}/top_level.txt +0 -0
udata/forms/validators.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
from urlextract import URLExtract
|
|
2
|
-
|
|
3
2
|
from wtforms import validators
|
|
4
3
|
from wtforms.validators import * # noqa
|
|
5
|
-
from wtforms.validators import
|
|
4
|
+
from wtforms.validators import StopValidation, ValidationError # noqa
|
|
6
5
|
|
|
7
6
|
|
|
8
7
|
def _(s):
|
|
@@ -10,10 +9,11 @@ def _(s):
|
|
|
10
9
|
|
|
11
10
|
|
|
12
11
|
class NoURLs(object):
|
|
13
|
-
|
|
12
|
+
"""
|
|
14
13
|
Check no url is present on this field
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, message=""):
|
|
17
17
|
self.extractor = URLExtract()
|
|
18
18
|
self.message = message
|
|
19
19
|
|
|
@@ -23,10 +23,11 @@ class NoURLs(object):
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
class RequiredIf(validators.DataRequired):
|
|
26
|
-
|
|
26
|
+
"""
|
|
27
27
|
A validator which makes a field required
|
|
28
28
|
only if another field is set and has a truthy value.
|
|
29
|
-
|
|
29
|
+
"""
|
|
30
|
+
|
|
30
31
|
def __init__(self, other_field_name, *args, **kwargs):
|
|
31
32
|
self.other_field_name = other_field_name
|
|
32
33
|
super(RequiredIf, self).__init__(*args, **kwargs)
|
|
@@ -34,16 +35,16 @@ class RequiredIf(validators.DataRequired):
|
|
|
34
35
|
def __call__(self, form, field):
|
|
35
36
|
other_field = form._fields.get(self.other_field_name)
|
|
36
37
|
if other_field is None:
|
|
37
|
-
raise Exception(
|
|
38
|
-
'No field named "%s" in form' % self.other_field_name)
|
|
38
|
+
raise Exception('No field named "%s" in form' % self.other_field_name)
|
|
39
39
|
if bool(other_field.data):
|
|
40
40
|
super(RequiredIf, self).__call__(form, field)
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
class Requires(object):
|
|
44
|
-
|
|
44
|
+
"""
|
|
45
45
|
A validator which makes a field required another field.
|
|
46
|
-
|
|
46
|
+
"""
|
|
47
|
+
|
|
47
48
|
def __init__(self, other_field_name, *args, **kwargs):
|
|
48
49
|
self.other_field_name = other_field_name
|
|
49
50
|
super(Requires, self).__init__(*args, **kwargs)
|
|
@@ -53,32 +54,28 @@ class Requires(object):
|
|
|
53
54
|
return
|
|
54
55
|
other_field = form._fields.get(self.other_field_name)
|
|
55
56
|
if other_field is None:
|
|
56
|
-
raise Exception(
|
|
57
|
-
'No field named "%s" in form' % self.other_field_name)
|
|
57
|
+
raise Exception('No field named "%s" in form' % self.other_field_name)
|
|
58
58
|
if not bool(other_field.data):
|
|
59
59
|
msg = field._('This field requires "%(name)s" to be set')
|
|
60
|
-
raise validators.ValidationError(
|
|
61
|
-
msg % {'name': field._(other_field.label.text)})
|
|
60
|
+
raise validators.ValidationError(msg % {"name": field._(other_field.label.text)})
|
|
62
61
|
|
|
63
62
|
|
|
64
63
|
class RequiredIfVal(validators.DataRequired):
|
|
65
|
-
|
|
64
|
+
"""
|
|
66
65
|
A validator which makes a field required
|
|
67
66
|
only if another field is set and has a specified value.
|
|
68
|
-
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
69
|
def __init__(self, other_field_name, expected_value, *args, **kwargs):
|
|
70
70
|
self.other_field_name = other_field_name
|
|
71
71
|
self.expected_values = (
|
|
72
|
-
expected_value
|
|
73
|
-
if isinstance(expected_value, (list, tuple))
|
|
74
|
-
else (expected_value,)
|
|
72
|
+
expected_value if isinstance(expected_value, (list, tuple)) else (expected_value,)
|
|
75
73
|
)
|
|
76
74
|
super(RequiredIfVal, self).__init__(*args, **kwargs)
|
|
77
75
|
|
|
78
76
|
def __call__(self, form, field):
|
|
79
77
|
other_field = form._fields.get(self.other_field_name)
|
|
80
78
|
if other_field is None:
|
|
81
|
-
raise Exception(
|
|
82
|
-
'No field named "%s" in form' % self.other_field_name)
|
|
79
|
+
raise Exception('No field named "%s" in form' % self.other_field_name)
|
|
83
80
|
if other_field.data in self.expected_values:
|
|
84
81
|
super(RequiredIfVal, self).__call__(form, field)
|
udata/forms/widgets.py
CHANGED
|
@@ -10,14 +10,10 @@ class WidgetHelper(object):
|
|
|
10
10
|
|
|
11
11
|
def __call__(self, field, **kwargs):
|
|
12
12
|
# Handle extra classes
|
|
13
|
-
classes = (kwargs.pop(
|
|
14
|
-
extra_classes = (
|
|
15
|
-
self.classes
|
|
16
|
-
if isinstance(self.classes, (list, tuple))
|
|
17
|
-
else [self.classes]
|
|
18
|
-
)
|
|
13
|
+
classes = (kwargs.pop("class", "") or kwargs.pop("class_", "")).split()
|
|
14
|
+
extra_classes = self.classes if isinstance(self.classes, (list, tuple)) else [self.classes]
|
|
19
15
|
classes.extend([cls for cls in extra_classes if cls not in classes])
|
|
20
|
-
kwargs[
|
|
16
|
+
kwargs["class"] = " ".join(classes)
|
|
21
17
|
|
|
22
18
|
# Handle defaults
|
|
23
19
|
for key, value in self.attributes.items():
|
|
@@ -35,19 +31,19 @@ class TextArea(WidgetHelper, widgets.TextArea):
|
|
|
35
31
|
|
|
36
32
|
|
|
37
33
|
class SelectPicker(WidgetHelper, widgets.Select):
|
|
38
|
-
classes =
|
|
34
|
+
classes = "selectpicker"
|
|
39
35
|
|
|
40
36
|
|
|
41
37
|
class MarkdownEditor(WidgetHelper, widgets.TextArea):
|
|
42
|
-
classes =
|
|
43
|
-
attributes = {
|
|
38
|
+
classes = "md"
|
|
39
|
+
attributes = {"rows": 8}
|
|
44
40
|
|
|
45
41
|
|
|
46
42
|
class DateRangePicker(WidgetHelper, widgets.HiddenInput):
|
|
47
|
-
classes =
|
|
43
|
+
classes = "dtpicker"
|
|
48
44
|
|
|
49
45
|
def __call__(self, field, **kwargs):
|
|
50
46
|
if field.data:
|
|
51
|
-
kwargs[
|
|
52
|
-
kwargs[
|
|
47
|
+
kwargs["data-start-date"] = field.data.start
|
|
48
|
+
kwargs["data-end-date"] = field.data.end
|
|
53
49
|
return super(DateRangePicker, self).__call__(field, **kwargs)
|
udata/frontend/__init__.py
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
import logging
|
|
3
|
-
import pkg_resources
|
|
4
|
-
|
|
5
|
-
from time import time
|
|
6
3
|
from importlib import import_module
|
|
4
|
+
from time import time
|
|
5
|
+
|
|
6
|
+
import pkg_resources
|
|
7
|
+
from flask import current_app
|
|
7
8
|
from jinja2 import pass_context
|
|
8
9
|
from markupsafe import Markup
|
|
9
|
-
from flask import current_app
|
|
10
10
|
|
|
11
11
|
from udata import assets, entrypoints
|
|
12
12
|
from udata.i18n import I18nBlueprint
|
|
13
13
|
|
|
14
|
-
from .markdown import UdataCleaner
|
|
15
|
-
|
|
14
|
+
from .markdown import UdataCleaner
|
|
15
|
+
from .markdown import init_app as init_markdown
|
|
16
16
|
|
|
17
17
|
log = logging.getLogger(__name__)
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
hook = I18nBlueprint(
|
|
20
|
+
hook = I18nBlueprint("hook", __name__)
|
|
21
21
|
|
|
22
22
|
_template_hooks = {}
|
|
23
23
|
|
|
@@ -27,15 +27,15 @@ def package_version(name):
|
|
|
27
27
|
return pkg_resources.get_distribution(name).version
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
@hook.app_template_global(name=
|
|
30
|
+
@hook.app_template_global(name="static")
|
|
31
31
|
def static_global(filename, _burst=True, **kwargs):
|
|
32
|
-
if current_app.config[
|
|
32
|
+
if current_app.config["DEBUG"] or current_app.config["TESTING"]:
|
|
33
33
|
burst = time()
|
|
34
34
|
else:
|
|
35
|
-
burst = package_version(
|
|
35
|
+
burst = package_version("udata")
|
|
36
36
|
if _burst:
|
|
37
|
-
kwargs[
|
|
38
|
-
return assets.cdn_for(
|
|
37
|
+
kwargs["_"] = burst
|
|
38
|
+
return assets.cdn_for("static", filename=filename, **kwargs)
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
def _wrapper(func, name=None, when=None):
|
|
@@ -50,8 +50,10 @@ def template_hook(func_or_name, when=None):
|
|
|
50
50
|
if callable(func_or_name):
|
|
51
51
|
return _wrapper(func_or_name)
|
|
52
52
|
elif isinstance(func_or_name, str):
|
|
53
|
+
|
|
53
54
|
def wrapper(func):
|
|
54
55
|
return _wrapper(func, func_or_name, when=when)
|
|
56
|
+
|
|
55
57
|
return wrapper
|
|
56
58
|
|
|
57
59
|
|
|
@@ -67,11 +69,13 @@ class HookRenderer:
|
|
|
67
69
|
self.kwargs = kwargs
|
|
68
70
|
|
|
69
71
|
def __html__(self):
|
|
70
|
-
return Markup(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
return Markup(
|
|
73
|
+
"".join(
|
|
74
|
+
f(self.ctx, *self.args, **self.kwargs)
|
|
75
|
+
for f, w in self.funcs
|
|
76
|
+
if w is None or w(self.ctx)
|
|
77
|
+
)
|
|
78
|
+
)
|
|
75
79
|
|
|
76
80
|
def __iter__(self):
|
|
77
81
|
for func, when in self.funcs:
|
|
@@ -82,20 +86,21 @@ class HookRenderer:
|
|
|
82
86
|
@pass_context
|
|
83
87
|
def render_template_hook(ctx, name, *args, **kwargs):
|
|
84
88
|
if not has_template_hook(name):
|
|
85
|
-
return
|
|
89
|
+
return ""
|
|
86
90
|
return HookRenderer(_template_hooks[name], ctx, *args, **kwargs)
|
|
87
91
|
|
|
88
92
|
|
|
89
93
|
@hook.app_context_processor
|
|
90
94
|
def inject_hooks():
|
|
91
95
|
return {
|
|
92
|
-
|
|
93
|
-
|
|
96
|
+
"hook": render_template_hook,
|
|
97
|
+
"has_hook": has_template_hook,
|
|
94
98
|
}
|
|
95
99
|
|
|
96
100
|
|
|
97
101
|
class SafeMarkup(Markup):
|
|
98
|
-
|
|
102
|
+
"""Markup object bypasses Jinja's escaping. This override allows to sanitize the resulting html."""
|
|
103
|
+
|
|
99
104
|
def __new__(cls, base, *args, **kwargs):
|
|
100
105
|
cleaner = UdataCleaner()
|
|
101
106
|
return super().__new__(cls, cleaner.clean(base), *args, **kwargs)
|
|
@@ -103,12 +108,12 @@ class SafeMarkup(Markup):
|
|
|
103
108
|
|
|
104
109
|
def _load_views(app, module):
|
|
105
110
|
views = module if inspect.ismodule(module) else import_module(module)
|
|
106
|
-
blueprint = getattr(views,
|
|
111
|
+
blueprint = getattr(views, "blueprint", None)
|
|
107
112
|
if blueprint:
|
|
108
113
|
app.register_blueprint(blueprint)
|
|
109
114
|
|
|
110
115
|
|
|
111
|
-
VIEWS = [
|
|
116
|
+
VIEWS = ["core.storages", "core.tags", "admin"]
|
|
112
117
|
|
|
113
118
|
|
|
114
119
|
def init_app(app, views=None):
|
|
@@ -117,16 +122,16 @@ def init_app(app, views=None):
|
|
|
117
122
|
init_markdown(app)
|
|
118
123
|
|
|
119
124
|
for view in views:
|
|
120
|
-
_load_views(app,
|
|
125
|
+
_load_views(app, "udata.{}.views".format(view))
|
|
121
126
|
|
|
122
127
|
# Load hook blueprint
|
|
123
128
|
app.register_blueprint(hook)
|
|
124
129
|
|
|
125
130
|
# Load all plugins views and blueprints
|
|
126
|
-
for module in entrypoints.get_enabled(
|
|
131
|
+
for module in entrypoints.get_enabled("udata.views", app).values():
|
|
127
132
|
_load_views(app, module)
|
|
128
133
|
|
|
129
134
|
# Load all plugins views and blueprints
|
|
130
|
-
for module in entrypoints.get_enabled(
|
|
135
|
+
for module in entrypoints.get_enabled("udata.front", app).values():
|
|
131
136
|
front_module = module if inspect.ismodule(module) else import_module(module)
|
|
132
137
|
front_module.init_app(app)
|
udata/frontend/csv.py
CHANGED
|
@@ -1,29 +1,26 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import itertools
|
|
1
3
|
import logging
|
|
2
|
-
|
|
4
|
+
from datetime import date, datetime
|
|
3
5
|
from io import StringIO
|
|
4
|
-
import itertools
|
|
5
|
-
import csv
|
|
6
|
-
|
|
7
|
-
from datetime import datetime, date
|
|
8
6
|
|
|
9
7
|
from flask import Response, stream_with_context
|
|
10
8
|
|
|
11
9
|
from udata.mongo import db
|
|
12
10
|
from udata.utils import recursive_get
|
|
13
11
|
|
|
14
|
-
|
|
15
12
|
log = logging.getLogger(__name__)
|
|
16
13
|
|
|
17
14
|
_adapters = {}
|
|
18
15
|
|
|
19
16
|
CONFIG = {
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
"delimiter": ";",
|
|
18
|
+
"quotechar": '"',
|
|
22
19
|
}
|
|
23
20
|
|
|
24
21
|
|
|
25
22
|
def safestr(value):
|
|
26
|
-
|
|
23
|
+
"""Ensure type to string serialization"""
|
|
27
24
|
if not value or isinstance(value, (int, float, bool)):
|
|
28
25
|
return value
|
|
29
26
|
elif isinstance(value, (date, datetime)):
|
|
@@ -33,7 +30,8 @@ def safestr(value):
|
|
|
33
30
|
|
|
34
31
|
|
|
35
32
|
class Adapter(object):
|
|
36
|
-
|
|
33
|
+
"""A Base model CSV adapter"""
|
|
34
|
+
|
|
37
35
|
fields = None
|
|
38
36
|
|
|
39
37
|
def __init__(self, queryset):
|
|
@@ -43,7 +41,7 @@ class Adapter(object):
|
|
|
43
41
|
def get_fields(self):
|
|
44
42
|
if not self._fields:
|
|
45
43
|
if not isinstance(self.fields, (list, tuple)):
|
|
46
|
-
raise ValueError(
|
|
44
|
+
raise ValueError("Unsupported fields format")
|
|
47
45
|
self._fields = []
|
|
48
46
|
for field in itertools.chain(self.fields, self.dynamic_fields()):
|
|
49
47
|
name = field if isinstance(field, str) else field[0]
|
|
@@ -58,39 +56,46 @@ class Adapter(object):
|
|
|
58
56
|
else:
|
|
59
57
|
field_tuple = (name, self.getter(*field))
|
|
60
58
|
except Exception as e: # Catch all errors intentionally.
|
|
61
|
-
log.exception(
|
|
62
|
-
name
|
|
59
|
+
log.exception(
|
|
60
|
+
"Error exporting CSV for {name}: {error_class} {error}".format(
|
|
61
|
+
name=self.__class__.__name__, error_class=e.__class__.__name__, error=e
|
|
62
|
+
),
|
|
63
|
+
stack_info=True,
|
|
64
|
+
)
|
|
63
65
|
self._fields.append(field_tuple)
|
|
64
66
|
return self._fields
|
|
65
67
|
|
|
66
68
|
def getter(self, name, getter=None):
|
|
67
69
|
if not getter:
|
|
68
|
-
method =
|
|
69
|
-
return (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return (
|
|
73
|
-
if isinstance(getter, str) else getter)
|
|
70
|
+
method = "field_{0}".format(name)
|
|
71
|
+
return (
|
|
72
|
+
getattr(self, method) if hasattr(self, method) else lambda o: recursive_get(o, name)
|
|
73
|
+
)
|
|
74
|
+
return (lambda o: recursive_get(o, getter)) if isinstance(getter, str) else getter
|
|
74
75
|
|
|
75
76
|
def header(self):
|
|
76
|
-
|
|
77
|
+
"""Generate the CSV header row"""
|
|
77
78
|
return [name for name, getter in self.get_fields()]
|
|
78
79
|
|
|
79
80
|
def rows(self):
|
|
80
|
-
|
|
81
|
+
"""Iterate over queryset objects"""
|
|
81
82
|
return (self.to_row(o) for o in self.queryset)
|
|
82
83
|
|
|
83
84
|
def to_row(self, obj):
|
|
84
|
-
|
|
85
|
+
"""Convert an object into a flat csv row"""
|
|
85
86
|
row = []
|
|
86
87
|
for name, getter in self.get_fields():
|
|
87
|
-
content =
|
|
88
|
+
content = ""
|
|
88
89
|
if getter is not None:
|
|
89
90
|
try:
|
|
90
91
|
content = safestr(getter(obj))
|
|
91
92
|
except Exception as e: # Catch all errors intentionally.
|
|
92
|
-
log.exception(
|
|
93
|
-
name
|
|
93
|
+
log.exception(
|
|
94
|
+
"Error exporting CSV for {name}: {error_class} {error}".format(
|
|
95
|
+
name=self.__class__.__name__, error_class=e.__class__.__name__, error=e
|
|
96
|
+
),
|
|
97
|
+
stack_info=True,
|
|
98
|
+
)
|
|
94
99
|
row.append(content)
|
|
95
100
|
return row
|
|
96
101
|
|
|
@@ -107,17 +112,17 @@ class NestedAdapter(Adapter):
|
|
|
107
112
|
self._nested_fields = None
|
|
108
113
|
|
|
109
114
|
def header(self):
|
|
110
|
-
|
|
111
|
-
return
|
|
112
|
-
|
|
115
|
+
"""Generate the CSV header row"""
|
|
116
|
+
return super(NestedAdapter, self).header() + [
|
|
117
|
+
name for name, getter in self.get_nested_fields()
|
|
118
|
+
]
|
|
113
119
|
|
|
114
120
|
def get_nested_fields(self):
|
|
115
121
|
if not self._nested_fields:
|
|
116
122
|
if not isinstance(self.nested_fields, (list, tuple)):
|
|
117
|
-
raise ValueError(
|
|
123
|
+
raise ValueError("Unsupported nested fields format")
|
|
118
124
|
self._nested_fields = []
|
|
119
|
-
for field in itertools.chain(self.nested_fields,
|
|
120
|
-
self.nested_dynamic_fields()):
|
|
125
|
+
for field in itertools.chain(self.nested_fields, self.nested_dynamic_fields()):
|
|
121
126
|
name = field if isinstance(field, str) else field[0]
|
|
122
127
|
# Retrieving (dynamically) fields is prone to errors,
|
|
123
128
|
# we don't want to break the CSV generation for a unique
|
|
@@ -130,33 +135,39 @@ class NestedAdapter(Adapter):
|
|
|
130
135
|
else:
|
|
131
136
|
field_tuple = (name, self.getter(*field))
|
|
132
137
|
except Exception as e: # Catch all errors intentionally.
|
|
133
|
-
log.exception(
|
|
134
|
-
name
|
|
138
|
+
log.exception(
|
|
139
|
+
"Error exporting CSV for {name}: {error_class} {error}".format(
|
|
140
|
+
name=self.__class__.__name__, error_class=e.__class__.__name__, error=e
|
|
141
|
+
),
|
|
142
|
+
stack_info=True,
|
|
143
|
+
)
|
|
135
144
|
self._nested_fields.append(field_tuple)
|
|
136
145
|
return self._nested_fields
|
|
137
146
|
|
|
138
147
|
def get_queryset(self):
|
|
139
|
-
return ((o, n)
|
|
140
|
-
for o in self.queryset
|
|
141
|
-
for n in getattr(o, self.attribute))
|
|
148
|
+
return ((o, n) for o in self.queryset for n in getattr(o, self.attribute))
|
|
142
149
|
|
|
143
150
|
def rows(self):
|
|
144
|
-
|
|
145
|
-
return (
|
|
146
|
-
|
|
147
|
-
|
|
151
|
+
"""Iterate over queryset objects"""
|
|
152
|
+
return (
|
|
153
|
+
self.nested_row(o, n) for o in self.queryset for n in getattr(o, self.attribute, [])
|
|
154
|
+
)
|
|
148
155
|
|
|
149
156
|
def nested_row(self, obj, nested):
|
|
150
|
-
|
|
157
|
+
"""Convert an object into a flat csv row"""
|
|
151
158
|
row = self.to_row(obj)
|
|
152
159
|
for name, getter in self.get_nested_fields():
|
|
153
|
-
content =
|
|
160
|
+
content = ""
|
|
154
161
|
if getter is not None:
|
|
155
162
|
try:
|
|
156
163
|
content = safestr(getter(nested))
|
|
157
164
|
except Exception as e: # Catch all errors intentionally.
|
|
158
|
-
log.exception(
|
|
159
|
-
name
|
|
165
|
+
log.exception(
|
|
166
|
+
"Error exporting CSV for {name}: {error_class} {error}".format(
|
|
167
|
+
name=self.__class__.__name__, error_class=e.__class__.__name__, error=e
|
|
168
|
+
),
|
|
169
|
+
stack_info=True,
|
|
170
|
+
)
|
|
160
171
|
row.append(content)
|
|
161
172
|
return row
|
|
162
173
|
|
|
@@ -165,10 +176,12 @@ class NestedAdapter(Adapter):
|
|
|
165
176
|
|
|
166
177
|
|
|
167
178
|
def adapter(model):
|
|
168
|
-
|
|
179
|
+
"""Register an adapter class for a given model"""
|
|
180
|
+
|
|
169
181
|
def inner(cls):
|
|
170
182
|
_adapters[model] = cls
|
|
171
183
|
return cls
|
|
184
|
+
|
|
172
185
|
return inner
|
|
173
186
|
|
|
174
187
|
|
|
@@ -181,24 +194,21 @@ def _metric_getter(key):
|
|
|
181
194
|
|
|
182
195
|
|
|
183
196
|
def metric_fields(cls):
|
|
184
|
-
return [
|
|
185
|
-
('metric.{0}'.format(key), _metric_getter(key))
|
|
186
|
-
for key in cls.__metrics_keys__
|
|
187
|
-
]
|
|
197
|
+
return [("metric.{0}".format(key), _metric_getter(key)) for key in cls.__metrics_keys__]
|
|
188
198
|
|
|
189
199
|
|
|
190
200
|
def get_writer(out):
|
|
191
|
-
|
|
201
|
+
"""Get a preconfigured CSV writer for a given output file"""
|
|
192
202
|
return csv.writer(out, quoting=csv.QUOTE_NONNUMERIC, **CONFIG)
|
|
193
203
|
|
|
194
204
|
|
|
195
205
|
def get_reader(infile):
|
|
196
|
-
|
|
206
|
+
"""Get a preconfigured CSV reader for a given input file"""
|
|
197
207
|
return csv.reader(infile, **CONFIG)
|
|
198
208
|
|
|
199
209
|
|
|
200
210
|
def yield_rows(adapter):
|
|
201
|
-
|
|
211
|
+
"""Yield a dataset catalog line by line"""
|
|
202
212
|
csvfile = StringIO()
|
|
203
213
|
writer = get_writer(csvfile)
|
|
204
214
|
# Generate header
|
|
@@ -223,20 +233,20 @@ def stream(queryset_or_adapter, basename=None):
|
|
|
223
233
|
adapter = queryset_or_adapter
|
|
224
234
|
elif isinstance(queryset_or_adapter, (list, tuple)):
|
|
225
235
|
if not queryset_or_adapter:
|
|
226
|
-
raise ValueError(
|
|
227
|
-
'Type detection is not possible with an empty list')
|
|
236
|
+
raise ValueError("Type detection is not possible with an empty list")
|
|
228
237
|
cls = _adapters.get(queryset_or_adapter[0].__class__)
|
|
229
238
|
adapter = cls(queryset_or_adapter)
|
|
230
239
|
elif isinstance(queryset_or_adapter, db.BaseQuerySet):
|
|
231
240
|
cls = _adapters.get(queryset_or_adapter._document)
|
|
232
241
|
adapter = cls(queryset_or_adapter)
|
|
233
242
|
else:
|
|
234
|
-
raise ValueError(
|
|
243
|
+
raise ValueError("Unsupported object type")
|
|
235
244
|
|
|
236
|
-
timestamp = datetime.utcnow().strftime(
|
|
245
|
+
timestamp = datetime.utcnow().strftime("%Y-%m-%d-%H-%M")
|
|
237
246
|
headers = {
|
|
238
|
-
|
|
239
|
-
basename or
|
|
247
|
+
"Content-Disposition": "attachment; filename={0}-{1}.csv".format(
|
|
248
|
+
basename or "export", timestamp
|
|
249
|
+
),
|
|
240
250
|
}
|
|
241
251
|
streamer = stream_with_context(yield_rows(adapter))
|
|
242
252
|
return Response(streamer, mimetype="text/csv", headers=headers)
|