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
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import pytest
|
|
2
|
-
|
|
3
2
|
from bleach._vendor import html5lib
|
|
4
3
|
from flask import render_template_string
|
|
5
4
|
|
|
5
|
+
from udata.frontend.markdown import EXCERPT_TOKEN, md, parse_html
|
|
6
6
|
from udata.utils import faker
|
|
7
|
-
from udata.frontend.markdown import md, parse_html, EXCERPT_TOKEN
|
|
8
7
|
|
|
9
8
|
parser = html5lib.HTMLParser(tree=html5lib.getTreeBuilder("dom"))
|
|
10
9
|
|
|
@@ -17,319 +16,340 @@ def assert_md_equal(value, expected):
|
|
|
17
16
|
|
|
18
17
|
@pytest.fixture
|
|
19
18
|
def assert_md(app):
|
|
20
|
-
def assertion(text, expected, url=
|
|
19
|
+
def assertion(text, expected, url="/"):
|
|
21
20
|
__tracebackhide__ = True
|
|
22
21
|
with app.test_request_context(url):
|
|
23
|
-
result = render_template_string(
|
|
22
|
+
result = render_template_string("{{ text|markdown }}", text=text)
|
|
24
23
|
assert_md_equal(result, expected)
|
|
24
|
+
|
|
25
25
|
return assertion
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
@pytest.fixture
|
|
29
29
|
def md2dom(app):
|
|
30
|
-
def helper(text, expected=None, url=
|
|
30
|
+
def helper(text, expected=None, url="/"):
|
|
31
31
|
__tracebackhide__ = True
|
|
32
32
|
with app.test_request_context(url):
|
|
33
|
-
result = render_template_string(
|
|
33
|
+
result = render_template_string("{{ text|markdown }}", text=text)
|
|
34
34
|
if expected:
|
|
35
35
|
assert_md_equal(result, expected)
|
|
36
36
|
return parser.parse(result)
|
|
37
|
+
|
|
37
38
|
return helper
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
@pytest.mark.frontend
|
|
41
42
|
class MarkdownTest:
|
|
42
43
|
def test_excerpt_is_not_removed(self, app):
|
|
43
|
-
with app.test_request_context(
|
|
44
|
+
with app.test_request_context("/"):
|
|
44
45
|
assert_md_equal(md(EXCERPT_TOKEN), EXCERPT_TOKEN)
|
|
45
46
|
|
|
46
47
|
def test_markdown_filter_with_none(self, app):
|
|
47
|
-
|
|
48
|
+
"""Markdown filter should not fails with None"""
|
|
48
49
|
text = None
|
|
49
|
-
with app.test_request_context(
|
|
50
|
-
result = render_template_string(
|
|
50
|
+
with app.test_request_context("/"):
|
|
51
|
+
result = render_template_string("{{ text|markdown }}", text=text)
|
|
51
52
|
|
|
52
|
-
assert result ==
|
|
53
|
+
assert result == ""
|
|
53
54
|
|
|
54
55
|
def test_markdown_links_nofollow(self, md2dom):
|
|
55
|
-
|
|
56
|
+
"""Markdown filter should render links as nofollow"""
|
|
56
57
|
text = '[example](http://example.net/ "Title")'
|
|
57
58
|
dom = md2dom(text)
|
|
58
|
-
el = dom.getElementsByTagName(
|
|
59
|
-
assert el.getAttribute(
|
|
60
|
-
assert el.getAttribute(
|
|
61
|
-
assert el.getAttribute(
|
|
62
|
-
assert el.firstChild.data ==
|
|
59
|
+
el = dom.getElementsByTagName("a")[0]
|
|
60
|
+
assert el.getAttribute("rel") == "nofollow"
|
|
61
|
+
assert el.getAttribute("href") == "http://example.net/"
|
|
62
|
+
assert el.getAttribute("title") == "Title"
|
|
63
|
+
assert el.firstChild.data == "example"
|
|
63
64
|
|
|
64
65
|
def test_markdown_linkify(self, md2dom):
|
|
65
|
-
|
|
66
|
-
text =
|
|
66
|
+
"""Markdown filter should transform urls to anchors"""
|
|
67
|
+
text = "http://example.net/"
|
|
67
68
|
dom = md2dom(text)
|
|
68
|
-
el = dom.getElementsByTagName(
|
|
69
|
-
assert el.getAttribute(
|
|
70
|
-
assert el.getAttribute(
|
|
71
|
-
assert el.firstChild.data ==
|
|
69
|
+
el = dom.getElementsByTagName("a")[0]
|
|
70
|
+
assert el.getAttribute("rel") == "nofollow"
|
|
71
|
+
assert el.getAttribute("href") == "http://example.net/"
|
|
72
|
+
assert el.firstChild.data == "http://example.net/"
|
|
72
73
|
|
|
73
74
|
def test_markdown_autolink(self, md2dom):
|
|
74
|
-
|
|
75
|
-
text =
|
|
75
|
+
"""Markdown filter should transform urls to anchors"""
|
|
76
|
+
text = "<http://example.net/>"
|
|
76
77
|
dom = md2dom(text)
|
|
77
|
-
el = dom.getElementsByTagName(
|
|
78
|
-
assert el.getAttribute(
|
|
79
|
-
assert el.getAttribute(
|
|
80
|
-
assert el.firstChild.data ==
|
|
78
|
+
el = dom.getElementsByTagName("a")[0]
|
|
79
|
+
assert el.getAttribute("rel") == "nofollow"
|
|
80
|
+
assert el.getAttribute("href") == "http://example.net/"
|
|
81
|
+
assert el.firstChild.data == "http://example.net/"
|
|
81
82
|
|
|
82
83
|
def test_markdown_linkify_angle_brackets(self, md2dom):
|
|
83
|
-
|
|
84
|
-
text =
|
|
84
|
+
"""Markdown filter should transform urls to anchors"""
|
|
85
|
+
text = "<http://example.net/path>"
|
|
85
86
|
dom = md2dom(text)
|
|
86
|
-
el = dom.getElementsByTagName(
|
|
87
|
-
assert el.getAttribute(
|
|
88
|
-
assert el.getAttribute(
|
|
89
|
-
assert el.firstChild.data ==
|
|
90
|
-
|
|
91
|
-
@pytest.mark.parametrize(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
87
|
+
el = dom.getElementsByTagName("a")[0]
|
|
88
|
+
assert el.getAttribute("rel") == "nofollow"
|
|
89
|
+
assert el.getAttribute("href") == "http://example.net/path"
|
|
90
|
+
assert el.firstChild.data == "http://example.net/path"
|
|
91
|
+
|
|
92
|
+
@pytest.mark.parametrize(
|
|
93
|
+
"link,expected",
|
|
94
|
+
[
|
|
95
|
+
("/", "http://local.test/"),
|
|
96
|
+
("bar", "http://local.test/bar"),
|
|
97
|
+
("/?tag=test&sort=-followers", "http://local.test/?tag=test&sort=-followers"),
|
|
98
|
+
],
|
|
99
|
+
)
|
|
95
100
|
def test_markdown_linkify_relative(self, md2dom, link, expected):
|
|
96
|
-
|
|
97
|
-
text = f
|
|
101
|
+
"""Markdown filter should transform relative urls to external ones"""
|
|
102
|
+
text = f"[foo]({link})"
|
|
98
103
|
dom = md2dom(text)
|
|
99
|
-
el = dom.getElementsByTagName(
|
|
100
|
-
assert el.getAttribute(
|
|
101
|
-
assert el.getAttribute(
|
|
102
|
-
assert el.getAttribute(
|
|
103
|
-
assert el.firstChild.data ==
|
|
104
|
+
el = dom.getElementsByTagName("a")[0]
|
|
105
|
+
assert el.getAttribute("rel") == ""
|
|
106
|
+
assert el.getAttribute("href") == expected
|
|
107
|
+
assert el.getAttribute("data-tooltip") == ""
|
|
108
|
+
assert el.firstChild.data == "foo"
|
|
104
109
|
|
|
105
110
|
def test_markdown_linkify_https(self, md2dom):
|
|
106
|
-
|
|
107
|
-
text =
|
|
108
|
-
dom = md2dom(text, url=
|
|
109
|
-
el = dom.getElementsByTagName(
|
|
110
|
-
assert el.getAttribute(
|
|
111
|
-
assert el.getAttribute(
|
|
112
|
-
assert el.getAttribute(
|
|
113
|
-
assert el.firstChild.data ==
|
|
111
|
+
"""Markdown filter should transform relative urls with HTTPS"""
|
|
112
|
+
text = "[foo](/foo)"
|
|
113
|
+
dom = md2dom(text, url="https://local.test")
|
|
114
|
+
el = dom.getElementsByTagName("a")[0]
|
|
115
|
+
assert el.getAttribute("rel") == ""
|
|
116
|
+
assert el.getAttribute("href") == "https://local.test/foo"
|
|
117
|
+
assert el.getAttribute("data-tooltip") == ""
|
|
118
|
+
assert el.firstChild.data == "foo"
|
|
114
119
|
|
|
115
120
|
def test_markdown_linkify_ftp(self, md2dom):
|
|
116
|
-
|
|
117
|
-
text =
|
|
121
|
+
"""Markdown filter should transform ftp urls"""
|
|
122
|
+
text = "[foo](ftp://random.net)"
|
|
118
123
|
dom = md2dom(text)
|
|
119
|
-
el = dom.getElementsByTagName(
|
|
120
|
-
assert el.getAttribute(
|
|
121
|
-
assert el.firstChild.data ==
|
|
124
|
+
el = dom.getElementsByTagName("a")[0]
|
|
125
|
+
assert el.getAttribute("href") == "ftp://random.net"
|
|
126
|
+
assert el.firstChild.data == "foo"
|
|
122
127
|
|
|
123
128
|
def test_markdown_linkify_relative_with_tooltip(self, app):
|
|
124
|
-
|
|
125
|
-
text =
|
|
126
|
-
with app.test_request_context(
|
|
127
|
-
result = render_template_string(
|
|
128
|
-
'{{ text|markdown(source_tooltip=True) }}', text=text)
|
|
129
|
+
"""Markdown filter should transform + add tooltip"""
|
|
130
|
+
text = "[foo](/)"
|
|
131
|
+
with app.test_request_context("/"):
|
|
132
|
+
result = render_template_string("{{ text|markdown(source_tooltip=True) }}", text=text)
|
|
129
133
|
parsed = parser.parse(result)
|
|
130
|
-
el = parsed.getElementsByTagName(
|
|
131
|
-
assert el.getAttribute(
|
|
132
|
-
assert el.getAttribute(
|
|
133
|
-
assert el.getAttribute(
|
|
134
|
-
assert el.firstChild.data ==
|
|
134
|
+
el = parsed.getElementsByTagName("a")[0]
|
|
135
|
+
assert el.getAttribute("rel") == ""
|
|
136
|
+
assert el.getAttribute("href") == "http://local.test/"
|
|
137
|
+
assert el.getAttribute("data-tooltip") == "Source"
|
|
138
|
+
assert el.firstChild.data == "foo"
|
|
135
139
|
|
|
136
140
|
def test_markdown_not_linkify_mails(self, md2dom):
|
|
137
|
-
|
|
138
|
-
text =
|
|
139
|
-
dom = md2dom(text,
|
|
140
|
-
assert dom.getElementsByTagName(
|
|
141
|
+
"""Markdown filter should not transform emails to anchors"""
|
|
142
|
+
text = "coucou@cmoi.fr"
|
|
143
|
+
dom = md2dom(text, "<p>coucou@cmoi.fr</p>")
|
|
144
|
+
assert dom.getElementsByTagName("a") == []
|
|
141
145
|
|
|
142
146
|
def test_markdown_linkify_within_pre(self, assert_md):
|
|
143
|
-
|
|
144
|
-
text =
|
|
145
|
-
assert_md(text,
|
|
147
|
+
"""Markdown filter should not transform urls into <pre> anchors"""
|
|
148
|
+
text = "<pre>http://example.net/</pre>"
|
|
149
|
+
assert_md(text, "<pre>http://example.net/</pre>")
|
|
146
150
|
|
|
147
151
|
def test_markdown_linkify_email_within_pre(self, assert_md):
|
|
148
|
-
|
|
149
|
-
text =
|
|
150
|
-
assert_md(text,
|
|
152
|
+
"""Markdown filter should not transform emails into <pre> anchors"""
|
|
153
|
+
text = "<pre>coucou@cmoi.fr</pre>"
|
|
154
|
+
assert_md(text, "<pre>coucou@cmoi.fr</pre>")
|
|
151
155
|
|
|
152
156
|
def test_bleach_sanitize(self, assert_md):
|
|
153
|
-
|
|
154
|
-
text =
|
|
155
|
-
assert_md(text,
|
|
157
|
+
"""Markdown filter should sanitize evil code"""
|
|
158
|
+
text = "an <script>evil()</script>"
|
|
159
|
+
assert_md(text, "<p>an <script>evil()</script></p>")
|
|
156
160
|
|
|
157
161
|
def test_soft_break(self, assert_md):
|
|
158
|
-
|
|
159
|
-
text =
|
|
160
|
-
assert_md(text,
|
|
162
|
+
"""Markdown should treat soft breaks as br tag"""
|
|
163
|
+
text = "line 1\nline 2"
|
|
164
|
+
assert_md(text, "<p>line 1<br>\nline 2</p>")
|
|
161
165
|
|
|
162
166
|
def test_gfm_tables(self, assert_md):
|
|
163
|
-
|
|
164
|
-
text =
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
167
|
+
"""Should render GFM tables"""
|
|
168
|
+
text = "\n".join(
|
|
169
|
+
(
|
|
170
|
+
"| first | second |",
|
|
171
|
+
"|-------|--------|",
|
|
172
|
+
"| value | value |",
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
expected = "\n".join(
|
|
176
|
+
(
|
|
177
|
+
"<table>",
|
|
178
|
+
"<thead>",
|
|
179
|
+
"<tr>",
|
|
180
|
+
"<th>first</th>",
|
|
181
|
+
"<th>second</th>",
|
|
182
|
+
"</tr>",
|
|
183
|
+
"</thead>",
|
|
184
|
+
"<tbody>",
|
|
185
|
+
"<tr>",
|
|
186
|
+
"<td>value</td>",
|
|
187
|
+
"<td>value</td>",
|
|
188
|
+
"</tr>",
|
|
189
|
+
"</tbody>",
|
|
190
|
+
"</table>",
|
|
191
|
+
)
|
|
192
|
+
)
|
|
185
193
|
assert_md(text, expected)
|
|
186
194
|
|
|
187
195
|
def test_gfm_strikethrough(self, assert_md):
|
|
188
|
-
|
|
189
|
-
text =
|
|
190
|
-
assert_md(text,
|
|
196
|
+
"""Should render GFM strikethrough (extension)"""
|
|
197
|
+
text = "~~Hi~~ Hello, world!"
|
|
198
|
+
assert_md(text, "<p><del>Hi</del> Hello, world!</p>")
|
|
191
199
|
|
|
192
200
|
def test_gfm_tagfilter(self, assert_md):
|
|
193
|
-
|
|
201
|
+
"""It should handle GFM tagfilter extension"""
|
|
194
202
|
# Test extracted from https://github.github.com/gfm/#disallowed-raw-html-extension-
|
|
195
|
-
text =
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
203
|
+
text = "\n".join(
|
|
204
|
+
(
|
|
205
|
+
"<strong> <title></title> <style></style> <em></em></strong>",
|
|
206
|
+
"<blockquote>",
|
|
207
|
+
" <xmp> is disallowed. <XMP> is also disallowed.",
|
|
208
|
+
"</blockquote>",
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
expected = "\n".join(
|
|
212
|
+
(
|
|
213
|
+
"<p><strong> <title></title> <style></style> <em></em></strong></p>",
|
|
214
|
+
"<blockquote>",
|
|
215
|
+
" <xmp> is disallowed. <XMP> is also disallowed.",
|
|
216
|
+
"</blockquote>",
|
|
217
|
+
)
|
|
218
|
+
)
|
|
207
219
|
assert_md(text, expected)
|
|
208
220
|
|
|
209
221
|
def test_gfm_tagfilter_legit(self, assert_md):
|
|
210
|
-
|
|
211
|
-
text =
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
222
|
+
"""It should not filter legit markup"""
|
|
223
|
+
text = "\n".join(
|
|
224
|
+
(
|
|
225
|
+
"> This is a blockquote",
|
|
226
|
+
"> with <script>evil()</script> inside",
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
expected = "\n".join(
|
|
230
|
+
(
|
|
231
|
+
"<blockquote><p>This is a blockquote<br>",
|
|
232
|
+
"with <script>evil()</script> inside</p>",
|
|
233
|
+
"</blockquote>",
|
|
234
|
+
)
|
|
235
|
+
)
|
|
220
236
|
assert_md(text, expected)
|
|
221
237
|
|
|
222
238
|
def test_collapsible(self, assert_md):
|
|
223
|
-
|
|
224
|
-
text =
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
239
|
+
"""It should not escape the collabsible HTML elem"""
|
|
240
|
+
text = "\n".join(
|
|
241
|
+
(
|
|
242
|
+
"<details>",
|
|
243
|
+
"<summary>TITLE</summary>",
|
|
244
|
+
"BODY CONTENT",
|
|
245
|
+
"</details>",
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
expected = "\n".join(
|
|
249
|
+
(
|
|
250
|
+
"<details>",
|
|
251
|
+
"<summary>TITLE</summary>",
|
|
252
|
+
"BODY CONTENT",
|
|
253
|
+
"</details>",
|
|
254
|
+
)
|
|
255
|
+
)
|
|
236
256
|
assert_md(text, expected)
|
|
237
257
|
|
|
258
|
+
|
|
238
259
|
@pytest.mark.frontend
|
|
239
260
|
class MdStripTest:
|
|
240
261
|
def test_mdstrip_filter(self, app):
|
|
241
|
-
|
|
242
|
-
text =
|
|
243
|
-
with app.test_request_context(
|
|
244
|
-
result = render_template_string(
|
|
262
|
+
"""mdstrip should truncate the text before rendering"""
|
|
263
|
+
text = "1 2 3 4 5 6 7 8 9 0"
|
|
264
|
+
with app.test_request_context("/"):
|
|
265
|
+
result = render_template_string("{{ text|mdstrip(7) }}", text=text)
|
|
245
266
|
|
|
246
|
-
assert result ==
|
|
267
|
+
assert result == "1 2 3…"
|
|
247
268
|
|
|
248
269
|
def test_mdstrip_filter_does_not_truncate_without_size(self, app):
|
|
249
|
-
|
|
250
|
-
text =
|
|
251
|
-
with app.test_request_context(
|
|
252
|
-
result = render_template_string(
|
|
270
|
+
"""mdstrip should not truncate by default"""
|
|
271
|
+
text = "aaaa " * 300
|
|
272
|
+
with app.test_request_context("/"):
|
|
273
|
+
result = render_template_string("{{ text|mdstrip }}", text=text)
|
|
253
274
|
|
|
254
275
|
assert result.strip() == text.strip()
|
|
255
276
|
|
|
256
277
|
def test_mdstrip_filter_with_none(self, app):
|
|
257
|
-
|
|
278
|
+
"""mdstrip filter should not fails with None"""
|
|
258
279
|
text = None
|
|
259
|
-
with app.test_request_context(
|
|
260
|
-
result = render_template_string(
|
|
280
|
+
with app.test_request_context("/"):
|
|
281
|
+
result = render_template_string("{{ text|mdstrip }}", text=text)
|
|
261
282
|
|
|
262
|
-
assert result ==
|
|
283
|
+
assert result == ""
|
|
263
284
|
|
|
264
285
|
def test_mdstrip_filter_with_excerpt(self, app):
|
|
265
|
-
|
|
266
|
-
text =
|
|
267
|
-
with app.test_request_context(
|
|
268
|
-
result = render_template_string(
|
|
269
|
-
'{{ text|mdstrip(20) }}', text=text)
|
|
286
|
+
"""mdstrip should truncate on token if shorter than required size"""
|
|
287
|
+
text = "".join(["excerpt", EXCERPT_TOKEN, "aaaa " * 10])
|
|
288
|
+
with app.test_request_context("/"):
|
|
289
|
+
result = render_template_string("{{ text|mdstrip(20) }}", text=text)
|
|
270
290
|
|
|
271
|
-
assert result ==
|
|
291
|
+
assert result == "excerpt"
|
|
272
292
|
|
|
273
293
|
def test_mdstrip_does_not_truncate_in_tags(self, app):
|
|
274
|
-
|
|
275
|
-
text =
|
|
276
|
-
with app.test_request_context(
|
|
277
|
-
result = render_template_string(
|
|
294
|
+
"""mdstrip should not truncate in middle of a tag"""
|
|
295
|
+
text = " Here. aaaaa"
|
|
296
|
+
with app.test_request_context("/"):
|
|
297
|
+
result = render_template_string("{{ text|mdstrip(5) }}", text=text)
|
|
278
298
|
|
|
279
|
-
assert result.strip() ==
|
|
299
|
+
assert result.strip() == "Here…"
|
|
280
300
|
|
|
281
301
|
def test_mdstrip_returns_unsafe_string(self, app):
|
|
282
|
-
|
|
283
|
-
text =
|
|
284
|
-
with app.test_request_context(
|
|
285
|
-
unsafe = render_template_string(
|
|
286
|
-
safe = render_template_string(
|
|
302
|
+
"""mdstrip should returns html compliants strings"""
|
|
303
|
+
text = "&é<script>"
|
|
304
|
+
with app.test_request_context("/"):
|
|
305
|
+
unsafe = render_template_string("{{ text|mdstrip }}", text=text)
|
|
306
|
+
safe = render_template_string("{{ text|mdstrip|safe }}", text=text)
|
|
287
307
|
|
|
288
|
-
assert unsafe.strip() ==
|
|
289
|
-
assert safe.strip() ==
|
|
308
|
+
assert unsafe.strip() == "&é<script>"
|
|
309
|
+
assert safe.strip() == "&é<script>"
|
|
290
310
|
|
|
291
311
|
def test_mdstrip_custom_end(self, app):
|
|
292
|
-
|
|
293
|
-
text =
|
|
312
|
+
"""mdstrip should allow a custom ending string"""
|
|
313
|
+
text = "1234567890"
|
|
294
314
|
template = '{{ text|mdstrip(5, "$") }}'
|
|
295
|
-
with app.test_request_context(
|
|
315
|
+
with app.test_request_context("/"):
|
|
296
316
|
result = render_template_string(template, text=text)
|
|
297
317
|
|
|
298
|
-
assert result.strip() ==
|
|
318
|
+
assert result.strip() == "1234$"
|
|
299
319
|
|
|
300
320
|
|
|
301
321
|
class HtmlToMarkdownTest:
|
|
302
322
|
def test_string_is_untouched(self):
|
|
303
|
-
assert parse_html(
|
|
323
|
+
assert parse_html("foo") == "foo"
|
|
304
324
|
|
|
305
325
|
def test_empty_string_is_untouched(self):
|
|
306
|
-
assert parse_html(
|
|
326
|
+
assert parse_html("") == ""
|
|
307
327
|
|
|
308
328
|
def test_none_is_empty_string(self):
|
|
309
|
-
assert parse_html(None) ==
|
|
329
|
+
assert parse_html(None) == ""
|
|
310
330
|
|
|
311
331
|
def test_parse_basic_html(self):
|
|
312
332
|
text = faker.paragraph()
|
|
313
|
-
html =
|
|
333
|
+
html = "<div>{0}</div>".format(text)
|
|
314
334
|
|
|
315
335
|
assert parse_html(html) == text
|
|
316
336
|
|
|
317
337
|
def test_content_is_stripped(self):
|
|
318
338
|
text = faker.paragraph()
|
|
319
|
-
spacer =
|
|
339
|
+
spacer = "\n " * 3
|
|
320
340
|
assert parse_html(spacer + text + spacer) == text
|
|
321
341
|
|
|
322
342
|
def test_parse_html_anchors(self):
|
|
323
343
|
html = '<a href="http://somewhere.com">title</a>'
|
|
324
|
-
assert parse_html(html) ==
|
|
344
|
+
assert parse_html(html) == "[title](http://somewhere.com)"
|
|
325
345
|
|
|
326
346
|
def test_parse_html_anchors_with_link_title(self):
|
|
327
|
-
url =
|
|
347
|
+
url = "http://somewhere.com/some/path"
|
|
328
348
|
html = '<a href="{0}">{0}</a>'.format(url)
|
|
329
|
-
assert parse_html(html) ==
|
|
349
|
+
assert parse_html(html) == "<{0}>".format(url)
|
|
330
350
|
|
|
331
351
|
def test_parse_html_anchors_with_attribute(self):
|
|
332
|
-
url =
|
|
352
|
+
url = "http://somewhere.com/some/path"
|
|
333
353
|
html = '<a href="{0}" target="_blank" title="a title">title</a>'
|
|
334
354
|
expected = '[title]({0} "a title")'
|
|
335
355
|
|