udata 12.0.2.dev15__py3-none-any.whl → 13.0.1.dev21__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.

Files changed (258) hide show
  1. udata/api/__init__.py +1 -0
  2. udata/api_fields.py +10 -4
  3. udata/app.py +11 -10
  4. udata/auth/__init__.py +9 -10
  5. udata/auth/mails.py +137 -45
  6. udata/auth/views.py +5 -12
  7. udata/commands/__init__.py +2 -3
  8. udata/commands/info.py +1 -3
  9. udata/commands/tests/test_fixtures.py +6 -3
  10. udata/core/access_type/api.py +18 -0
  11. udata/core/access_type/constants.py +98 -0
  12. udata/core/access_type/models.py +44 -0
  13. udata/core/activity/models.py +1 -1
  14. udata/core/badges/models.py +1 -1
  15. udata/core/badges/tasks.py +35 -1
  16. udata/core/badges/tests/test_commands.py +2 -4
  17. udata/core/badges/tests/test_model.py +2 -2
  18. udata/core/badges/tests/test_tasks.py +55 -0
  19. udata/core/constants.py +1 -0
  20. udata/core/contact_point/models.py +8 -0
  21. udata/core/dataservices/api.py +3 -3
  22. udata/core/dataservices/apiv2.py +3 -1
  23. udata/core/dataservices/constants.py +0 -29
  24. udata/core/dataservices/models.py +44 -44
  25. udata/core/dataservices/rdf.py +2 -1
  26. udata/core/dataservices/search.py +5 -9
  27. udata/core/dataservices/tasks.py +33 -0
  28. udata/core/dataset/api_fields.py +11 -0
  29. udata/core/dataset/apiv2.py +11 -0
  30. udata/core/dataset/constants.py +0 -1
  31. udata/core/dataset/forms.py +29 -0
  32. udata/core/dataset/models.py +16 -4
  33. udata/core/dataset/rdf.py +2 -1
  34. udata/core/dataset/search.py +2 -2
  35. udata/core/dataset/tasks.py +86 -8
  36. udata/core/discussions/mails.py +63 -0
  37. udata/core/discussions/tasks.py +4 -18
  38. udata/core/metrics/__init__.py +0 -6
  39. udata/core/organization/api.py +3 -1
  40. udata/core/organization/mails.py +144 -0
  41. udata/core/organization/models.py +2 -1
  42. udata/core/organization/search.py +1 -1
  43. udata/core/organization/tasks.py +21 -49
  44. udata/core/pages/tests/test_api.py +0 -2
  45. udata/core/reuse/api.py +27 -1
  46. udata/core/reuse/mails.py +21 -0
  47. udata/core/reuse/models.py +10 -1
  48. udata/core/reuse/search.py +1 -1
  49. udata/core/reuse/tasks.py +2 -3
  50. udata/core/site/models.py +2 -6
  51. udata/core/spatial/tests/test_api.py +17 -20
  52. udata/core/spatial/tests/test_models.py +3 -3
  53. udata/core/user/mails.py +54 -0
  54. udata/core/user/models.py +2 -3
  55. udata/core/user/tasks.py +8 -23
  56. udata/core/user/tests/test_user_model.py +2 -6
  57. udata/entrypoints.py +0 -5
  58. udata/features/identicon/tests/test_backends.py +3 -13
  59. udata/forms/fields.py +3 -3
  60. udata/forms/widgets.py +2 -2
  61. udata/frontend/__init__.py +3 -32
  62. udata/harvest/actions.py +4 -9
  63. udata/harvest/api.py +5 -14
  64. udata/harvest/backends/__init__.py +20 -11
  65. udata/harvest/backends/base.py +2 -2
  66. udata/harvest/backends/ckan/harvesters.py +2 -1
  67. udata/harvest/backends/dcat.py +3 -0
  68. udata/harvest/backends/maaf.py +1 -0
  69. udata/harvest/commands.py +6 -4
  70. udata/harvest/forms.py +9 -6
  71. udata/harvest/tasks.py +3 -5
  72. udata/harvest/tests/ckan/test_ckan_backend.py +300 -337
  73. udata/harvest/tests/ckan/test_ckan_backend_errors.py +94 -99
  74. udata/harvest/tests/ckan/test_ckan_backend_filters.py +128 -122
  75. udata/harvest/tests/ckan/test_dkan_backend.py +39 -51
  76. udata/harvest/tests/dcat/datara--5a26b0f6-0ccf-46ad-ac58-734054b91977.rdf.xml +255 -0
  77. udata/harvest/tests/dcat/datara--f40c3860-7236-4b30-a141-23b8ae33f7b2.rdf.xml +289 -0
  78. udata/harvest/tests/factories.py +1 -1
  79. udata/harvest/tests/test_actions.py +11 -9
  80. udata/harvest/tests/test_api.py +4 -5
  81. udata/harvest/tests/test_base_backend.py +5 -4
  82. udata/harvest/tests/test_dcat_backend.py +50 -19
  83. udata/harvest/tests/test_models.py +2 -4
  84. udata/harvest/tests/test_notifications.py +2 -4
  85. udata/harvest/tests/test_tasks.py +2 -3
  86. udata/mail.py +90 -53
  87. udata/migrations/2025-01-05-dataservices-fields-changes.py +8 -14
  88. udata/migrations/2025-10-21-remove-ckan-harvest-modified-at.py +28 -0
  89. udata/migrations/2025-10-29-harvesters-sources-integrity.py +27 -0
  90. udata/mongo/taglist_field.py +3 -3
  91. udata/rdf.py +32 -15
  92. udata/sentry.py +3 -4
  93. udata/settings.py +7 -2
  94. udata/tags.py +5 -5
  95. udata/tasks.py +3 -3
  96. udata/templates/mail/message.html +65 -0
  97. udata/templates/mail/message.txt +16 -0
  98. udata/tests/__init__.py +40 -58
  99. udata/tests/api/__init__.py +87 -2
  100. udata/tests/api/test_activities_api.py +17 -23
  101. udata/tests/api/test_auth_api.py +2 -4
  102. udata/tests/api/test_contact_points.py +48 -54
  103. udata/tests/api/test_dataservices_api.py +57 -37
  104. udata/tests/api/test_datasets_api.py +146 -49
  105. udata/tests/api/test_me_api.py +4 -6
  106. udata/tests/api/test_organizations_api.py +19 -38
  107. udata/tests/api/test_reports_api.py +0 -4
  108. udata/tests/api/test_reuses_api.py +92 -19
  109. udata/tests/api/test_security_api.py +124 -0
  110. udata/tests/api/test_swagger.py +2 -3
  111. udata/tests/api/test_tags_api.py +6 -7
  112. udata/tests/api/test_transfer_api.py +0 -2
  113. udata/tests/api/test_user_api.py +8 -10
  114. udata/tests/apiv2/test_datasets.py +0 -4
  115. udata/tests/apiv2/test_me_api.py +0 -2
  116. udata/tests/apiv2/test_organizations.py +0 -2
  117. udata/tests/apiv2/test_swagger.py +2 -3
  118. udata/tests/apiv2/test_topics.py +0 -2
  119. udata/tests/cli/test_cli_base.py +14 -12
  120. udata/tests/cli/test_db_cli.py +51 -54
  121. udata/tests/contact_point/test_contact_point_models.py +2 -2
  122. udata/tests/dataservice/test_csv_adapter.py +2 -5
  123. udata/tests/dataservice/test_dataservice_rdf.py +8 -6
  124. udata/tests/dataservice/test_dataservice_tasks.py +36 -38
  125. udata/tests/dataset/test_csv_adapter.py +2 -5
  126. udata/tests/dataset/test_dataset_actions.py +2 -4
  127. udata/tests/dataset/test_dataset_commands.py +2 -4
  128. udata/tests/dataset/test_dataset_events.py +3 -3
  129. udata/tests/dataset/test_dataset_model.py +6 -7
  130. udata/tests/dataset/test_dataset_rdf.py +201 -12
  131. udata/tests/dataset/test_dataset_recommendations.py +2 -2
  132. udata/tests/dataset/test_dataset_tasks.py +66 -68
  133. udata/tests/dataset/test_resource_preview.py +39 -48
  134. udata/tests/dataset/test_transport_tasks.py +2 -2
  135. udata/tests/features/territories/__init__.py +0 -6
  136. udata/tests/features/territories/test_territories_api.py +25 -24
  137. udata/tests/forms/test_current_user_field.py +2 -2
  138. udata/tests/forms/test_dict_field.py +2 -4
  139. udata/tests/forms/test_extras_fields.py +2 -3
  140. udata/tests/forms/test_image_field.py +2 -2
  141. udata/tests/forms/test_model_field.py +2 -4
  142. udata/tests/forms/test_publish_as_field.py +2 -4
  143. udata/tests/forms/test_user_forms.py +26 -29
  144. udata/tests/frontend/test_auth.py +2 -3
  145. udata/tests/frontend/test_csv.py +5 -6
  146. udata/tests/frontend/test_error_handlers.py +2 -3
  147. udata/tests/frontend/test_hooks.py +5 -7
  148. udata/tests/frontend/test_markdown.py +3 -4
  149. udata/tests/helpers.py +2 -7
  150. udata/tests/metrics/test_metrics.py +52 -48
  151. udata/tests/metrics/test_tasks.py +154 -150
  152. udata/tests/organization/test_csv_adapter.py +2 -5
  153. udata/tests/organization/test_notifications.py +2 -4
  154. udata/tests/organization/test_organization_model.py +3 -4
  155. udata/tests/organization/test_organization_rdf.py +2 -8
  156. udata/tests/plugin.py +6 -110
  157. udata/tests/reuse/test_reuse_model.py +3 -4
  158. udata/tests/site/test_site_api.py +0 -2
  159. udata/tests/site/test_site_csv_exports.py +0 -2
  160. udata/tests/site/test_site_metrics.py +2 -4
  161. udata/tests/site/test_site_model.py +2 -2
  162. udata/tests/site/test_site_rdf.py +4 -7
  163. udata/tests/test_activity.py +3 -3
  164. udata/tests/test_api_fields.py +6 -9
  165. udata/tests/test_cors.py +0 -2
  166. udata/tests/test_dcat_commands.py +2 -3
  167. udata/tests/test_discussions.py +2 -7
  168. udata/tests/test_mail.py +150 -114
  169. udata/tests/test_migrations.py +413 -419
  170. udata/tests/test_model.py +10 -11
  171. udata/tests/test_notifications.py +2 -3
  172. udata/tests/test_owned.py +3 -3
  173. udata/tests/test_rdf.py +19 -15
  174. udata/tests/test_routing.py +5 -5
  175. udata/tests/test_storages.py +6 -5
  176. udata/tests/test_tags.py +2 -4
  177. udata/tests/test_topics.py +2 -4
  178. udata/tests/test_transfer.py +4 -5
  179. udata/tests/topic/test_topic_tasks.py +25 -27
  180. udata/tests/user/test_user_rdf.py +2 -8
  181. udata/tests/user/test_user_tasks.py +3 -5
  182. udata/tests/workers/test_jobs_commands.py +2 -2
  183. udata/tests/workers/test_tasks_routing.py +27 -27
  184. udata/translations/ar/LC_MESSAGES/udata.mo +0 -0
  185. udata/translations/ar/LC_MESSAGES/udata.po +369 -435
  186. udata/translations/de/LC_MESSAGES/udata.mo +0 -0
  187. udata/translations/de/LC_MESSAGES/udata.po +371 -437
  188. udata/translations/es/LC_MESSAGES/udata.mo +0 -0
  189. udata/translations/es/LC_MESSAGES/udata.po +369 -435
  190. udata/translations/fr/LC_MESSAGES/udata.mo +0 -0
  191. udata/translations/fr/LC_MESSAGES/udata.po +381 -447
  192. udata/translations/it/LC_MESSAGES/udata.mo +0 -0
  193. udata/translations/it/LC_MESSAGES/udata.po +371 -437
  194. udata/translations/pt/LC_MESSAGES/udata.mo +0 -0
  195. udata/translations/pt/LC_MESSAGES/udata.po +371 -437
  196. udata/translations/sr/LC_MESSAGES/udata.mo +0 -0
  197. udata/translations/sr/LC_MESSAGES/udata.po +372 -438
  198. udata/translations/udata.pot +379 -440
  199. udata/utils.py +14 -2
  200. {udata-12.0.2.dev15.dist-info → udata-13.0.1.dev21.dist-info}/METADATA +1 -2
  201. {udata-12.0.2.dev15.dist-info → udata-13.0.1.dev21.dist-info}/RECORD +205 -242
  202. udata/templates/mail/account_deleted.html +0 -5
  203. udata/templates/mail/account_deleted.txt +0 -6
  204. udata/templates/mail/account_inactivity.html +0 -40
  205. udata/templates/mail/account_inactivity.txt +0 -31
  206. udata/templates/mail/badge_added_association.html +0 -33
  207. udata/templates/mail/badge_added_association.txt +0 -11
  208. udata/templates/mail/badge_added_certified.html +0 -33
  209. udata/templates/mail/badge_added_certified.txt +0 -11
  210. udata/templates/mail/badge_added_company.html +0 -33
  211. udata/templates/mail/badge_added_company.txt +0 -11
  212. udata/templates/mail/badge_added_local_authority.html +0 -33
  213. udata/templates/mail/badge_added_local_authority.txt +0 -11
  214. udata/templates/mail/badge_added_public_service.html +0 -33
  215. udata/templates/mail/badge_added_public_service.txt +0 -11
  216. udata/templates/mail/discussion_closed.html +0 -47
  217. udata/templates/mail/discussion_closed.txt +0 -16
  218. udata/templates/mail/inactive_account_deleted.html +0 -5
  219. udata/templates/mail/inactive_account_deleted.txt +0 -6
  220. udata/templates/mail/membership_refused.html +0 -20
  221. udata/templates/mail/membership_refused.txt +0 -11
  222. udata/templates/mail/membership_request.html +0 -46
  223. udata/templates/mail/membership_request.txt +0 -12
  224. udata/templates/mail/new_discussion.html +0 -44
  225. udata/templates/mail/new_discussion.txt +0 -15
  226. udata/templates/mail/new_discussion_comment.html +0 -45
  227. udata/templates/mail/new_discussion_comment.txt +0 -16
  228. udata/templates/mail/new_member.html +0 -27
  229. udata/templates/mail/new_member.txt +0 -11
  230. udata/templates/mail/new_reuse.html +0 -37
  231. udata/templates/mail/new_reuse.txt +0 -9
  232. udata/templates/mail/test.html +0 -6
  233. udata/templates/mail/test.txt +0 -6
  234. udata/templates/mail/user_mail_card.html +0 -26
  235. udata/templates/security/email/base.html +0 -105
  236. udata/templates/security/email/base.txt +0 -6
  237. udata/templates/security/email/button.html +0 -3
  238. udata/templates/security/email/change_notice.html +0 -22
  239. udata/templates/security/email/change_notice.txt +0 -8
  240. udata/templates/security/email/confirmation_instructions.html +0 -20
  241. udata/templates/security/email/confirmation_instructions.txt +0 -7
  242. udata/templates/security/email/login_instructions.html +0 -19
  243. udata/templates/security/email/login_instructions.txt +0 -7
  244. udata/templates/security/email/reset_instructions.html +0 -24
  245. udata/templates/security/email/reset_instructions.txt +0 -9
  246. udata/templates/security/email/reset_notice.html +0 -11
  247. udata/templates/security/email/reset_notice.txt +0 -4
  248. udata/templates/security/email/welcome.html +0 -24
  249. udata/templates/security/email/welcome.txt +0 -9
  250. udata/templates/security/email/welcome_existing.html +0 -32
  251. udata/templates/security/email/welcome_existing.txt +0 -14
  252. udata/terms.md +0 -6
  253. udata/tests/frontend/__init__.py +0 -23
  254. udata/tests/metrics/conftest.py +0 -15
  255. {udata-12.0.2.dev15.dist-info → udata-13.0.1.dev21.dist-info}/WHEEL +0 -0
  256. {udata-12.0.2.dev15.dist-info → udata-13.0.1.dev21.dist-info}/entry_points.txt +0 -0
  257. {udata-12.0.2.dev15.dist-info → udata-13.0.1.dev21.dist-info}/licenses/LICENSE +0 -0
  258. {udata-12.0.2.dev15.dist-info → udata-13.0.1.dev21.dist-info}/top_level.txt +0 -0
@@ -4,7 +4,6 @@ from datetime import date
4
4
 
5
5
  import pytest
6
6
 
7
- from udata.app import create_app
8
7
  from udata.core.dataset.constants import UpdateFrequency
9
8
  from udata.core.organization.factories import OrganizationFactory
10
9
  from udata.core.spatial.factories import GeoZoneFactory
@@ -13,34 +12,14 @@ from udata.harvest.backends.ckan.harvesters import ALLOWED_RESOURCE_TYPES
13
12
  from udata.harvest.backends.ckan.schemas.ckan import RESOURCE_TYPES
14
13
  from udata.harvest.tests.factories import HarvestSourceFactory
15
14
  from udata.models import Dataset
16
- from udata.settings import Defaults, Testing
17
- from udata.tests.plugin import drop_db
15
+ from udata.tests.api import PytestOnlyDBTestCase
18
16
  from udata.utils import faker
19
17
 
20
18
 
21
- class CkanSettings(Testing):
22
- PLUGINS = ["ckan"]
23
-
24
-
25
19
  @pytest.fixture
26
- def app(request):
27
- """Create an udata app."""
28
- app = create_app(Defaults, override=CkanSettings)
29
- with app.app_context():
30
- drop_db(app)
31
- yield app
32
- with app.app_context():
33
- drop_db(app)
34
-
35
-
36
- @pytest.fixture
37
- def source(app, ckan):
38
- """
39
- Create an harvest source for an organization.
40
- """
41
- with app.app_context():
42
- org = OrganizationFactory()
43
- return HarvestSourceFactory(backend="ckan", url=ckan.BASE_URL, organization=org)
20
+ def source(ckan):
21
+ org = OrganizationFactory()
22
+ return HarvestSourceFactory(backend="ckan", url=ckan.BASE_URL, organization=org)
44
23
 
45
24
 
46
25
  def ckan_package(data):
@@ -72,7 +51,7 @@ def ckan_package(data):
72
51
 
73
52
 
74
53
  @pytest.fixture
75
- def harvest_ckan(request, source, ckan, app, rmock):
54
+ def harvest_ckan(request, source, ckan, rmock):
76
55
  """
77
56
  This fixture performs the harvesting and return the data, result
78
57
  and kwargs for this test case
@@ -96,11 +75,10 @@ def harvest_ckan(request, source, ckan, app, rmock):
96
75
  headers={"Content-Type": "application/json"},
97
76
  )
98
77
 
99
- with app.app_context():
100
- actions.run(source)
101
- source.reload()
102
- job = source.get_last_job()
103
- assert len(job.items) == 1
78
+ actions.run(source)
79
+ source.reload()
80
+ job = source.get_last_job()
81
+ assert len(job.items) == 1
104
82
 
105
83
  return data, result, kwargs
106
84
 
@@ -223,9 +201,8 @@ def spatial_geom_multipolygon(resource_data):
223
201
 
224
202
 
225
203
  @pytest.fixture
226
- def known_spatial_text_name(app, resource_data):
227
- with app.app_context():
228
- zone = GeoZoneFactory()
204
+ def known_spatial_text_name(resource_data):
205
+ zone = GeoZoneFactory()
229
206
  data = {
230
207
  "name": faker.unique_string(),
231
208
  "title": faker.sentence(),
@@ -237,9 +214,8 @@ def known_spatial_text_name(app, resource_data):
237
214
 
238
215
 
239
216
  @pytest.fixture
240
- def known_spatial_text_slug(app, resource_data):
241
- with app.app_context():
242
- zone = GeoZoneFactory()
217
+ def known_spatial_text_slug(resource_data):
218
+ zone = GeoZoneFactory()
243
219
  data = {
244
220
  "name": faker.unique_string(),
245
221
  "title": faker.sentence(),
@@ -251,10 +227,9 @@ def known_spatial_text_slug(app, resource_data):
251
227
 
252
228
 
253
229
  @pytest.fixture
254
- def multiple_known_spatial_text(app, resource_data):
230
+ def multiple_known_spatial_text(resource_data):
255
231
  name = faker.word()
256
- with app.app_context():
257
- GeoZoneFactory.create_batch(2, name=name)
232
+ GeoZoneFactory.create_batch(2, name=name)
258
233
  data = {
259
234
  "name": faker.unique_string(),
260
235
  "title": faker.sentence(),
@@ -389,310 +364,298 @@ def empty_extras(resource_data):
389
364
  ##############################################################################
390
365
 
391
366
 
392
- @pytest.mark.ckan_data("minimal")
393
- def test_minimal_metadata(data, result, kwargs):
394
- resource_url = kwargs["resource_url"]
395
-
396
- dataset = dataset_for(result)
397
- assert dataset.title == data["title"]
398
- assert dataset.description == data["notes"]
399
- assert dataset.harvest.remote_id == result["result"]["id"]
400
- assert dataset.harvest.domain == "localhost"
401
- assert dataset.harvest.ckan_name == data["name"]
402
- assert len(dataset.resources) == 1
403
-
404
- resource = dataset.resources[0]
405
- assert resource.url == resource_url
406
-
407
-
408
- @pytest.mark.ckan_data("all_metadata")
409
- def test_all_metadata(data, result):
410
- resource_data = data["resources"][0]
411
- resource_result = result["result"]["resources"][0]
412
-
413
- dataset = dataset_for(result)
414
- assert dataset.title == data["title"]
415
- assert dataset.description == data["notes"]
416
- assert set(dataset.tags) == set([t["name"] for t in data["tags"]])
417
- assert dataset.harvest.remote_id == result["result"]["id"]
418
- assert dataset.harvest.domain == "localhost"
419
- assert dataset.harvest.ckan_name == data["name"]
420
- assert len(dataset.resources) == 1
421
-
422
- resource = dataset.resources[0]
423
- assert resource.title == resource_data["name"]
424
- assert resource.description == resource_data["description"]
425
- assert resource.url == resource_data["url"]
426
- # Use result because format is normalized by CKAN
427
- assert resource.format == resource_result["format"].lower()
428
- assert resource.mime == resource_data["mimetype"].lower()
429
- assert resource.harvest.issued_at.date() == date(2022, 9, 29)
430
- assert resource.harvest.modified_at.date() == date(2022, 9, 30)
431
-
432
-
433
- @pytest.mark.ckan_data("spatial_geom_polygon")
434
- def test_geospatial_geom_polygon(result, kwargs):
435
- polygon = kwargs["polygon"]
436
- dataset = dataset_for(result)
437
-
438
- assert dataset.spatial.geom == {
439
- "type": "MultiPolygon",
440
- "coordinates": [polygon["coordinates"]],
441
- }
442
-
443
-
444
- @pytest.mark.ckan_data("spatial_geom_multipolygon")
445
- def test_geospatial_geom_multipolygon(result, kwargs):
446
- multipolygon = kwargs["multipolygon"]
447
-
448
- dataset = dataset_for(result)
449
- assert dataset.spatial.geom == multipolygon
450
-
451
-
452
- @pytest.mark.ckan_data("skipped_no_resources")
453
- def test_skip_no_resources(source, result):
454
- job = source.get_last_job()
455
- item = job_item_for(job, result)
456
- assert item.status == "skipped"
457
- assert dataset_for(result) is None
458
-
459
-
460
- @pytest.mark.ckan_data("ckan_url_is_url")
461
- def test_ckan_url_is_url(data, result):
462
- dataset = dataset_for(result)
463
- assert dataset.harvest.remote_url == data["url"]
464
- assert dataset.harvest.ckan_source is None
465
-
466
-
467
- @pytest.mark.ckan_data("ckan_url_is_a_string")
468
- def test_ckan_url_is_string(ckan, data, result):
469
- dataset = dataset_for(result)
470
- expected_url = "{0}/dataset/{1}".format(ckan.BASE_URL, data["name"])
471
- assert dataset.harvest.remote_url == expected_url
472
- assert dataset.harvest.ckan_source == data["url"]
473
-
474
-
475
- @pytest.mark.ckan_data("frequency_as_rdf_uri")
476
- def test_can_parse_frequency_as_uri(result, kwargs):
477
- dataset = dataset_for(result)
478
- assert dataset.frequency == kwargs["expected"]
479
- assert "ckan:frequency" not in dataset.extras
480
-
481
-
482
- @pytest.mark.ckan_data("frequency_as_exact_match")
483
- def test_can_parse_frequency_as_exact_match(result, kwargs):
484
- dataset = dataset_for(result)
485
- assert dataset.frequency == kwargs["expected"]
486
- assert "ckan:frequency" not in dataset.extras
487
-
488
-
489
- @pytest.mark.ckan_data("frequency_as_unknown_value")
490
- def test_can_parse_frequency_as_unknown_value(result, kwargs):
491
- dataset = dataset_for(result)
492
- assert dataset.extras["ckan:frequency"] == kwargs["expected"]
493
- assert dataset.frequency is None
494
-
495
-
496
- @pytest.mark.ckan_data("empty_extras")
497
- def test_skip_empty_extras(result):
498
- dataset = dataset_for(result)
499
- assert "none" not in dataset.extras
500
- assert "blank" not in dataset.extras
501
- assert "spaces" not in dataset.extras
502
-
503
-
504
- @pytest.mark.ckan_data("known_spatial_text_name")
505
- def test_known_spatial_text_name(result, kwargs):
506
- zone = kwargs["zone"]
507
- dataset = dataset_for(result)
508
- assert zone in dataset.spatial.zones
509
- assert "ckan:spatial-text" not in dataset.extras
510
-
511
-
512
- @pytest.mark.ckan_data("known_spatial_text_slug")
513
- def test_known_spatial_text_slug(result, kwargs):
514
- zone = kwargs["zone"]
515
- dataset = dataset_for(result)
516
- assert zone in dataset.spatial.zones
517
- assert "ckan:spatial-text" not in dataset.extras
518
-
519
-
520
- @pytest.mark.ckan_data("multiple_known_spatial_text")
521
- def test_store_unsure_spatial_text_as_extra(result, kwargs):
522
- dataset = dataset_for(result)
523
- assert dataset.extras["ckan:spatial-text"] == kwargs["name"]
524
- assert dataset.spatial is None
525
-
367
+ @pytest.mark.options(HARVESTER_BACKENDS=["ckan"])
368
+ class CkanBackendTest(PytestOnlyDBTestCase):
369
+ @pytest.mark.ckan_data("minimal")
370
+ def test_minimal_metadata(self, data, result, kwargs):
371
+ resource_url = kwargs["resource_url"]
372
+
373
+ dataset = dataset_for(result)
374
+ assert dataset.title == data["title"]
375
+ assert dataset.description == data["notes"]
376
+ assert dataset.harvest.remote_id == result["result"]["id"]
377
+ assert dataset.harvest.domain == "localhost"
378
+ assert dataset.harvest.ckan_name == data["name"]
379
+ assert len(dataset.resources) == 1
380
+
381
+ resource = dataset.resources[0]
382
+ assert resource.url == resource_url
383
+
384
+ @pytest.mark.ckan_data("all_metadata")
385
+ def test_all_metadata(self, data, result):
386
+ resource_data = data["resources"][0]
387
+ resource_result = result["result"]["resources"][0]
388
+
389
+ dataset = dataset_for(result)
390
+ assert dataset.title == data["title"]
391
+ assert dataset.description == data["notes"]
392
+ assert set(dataset.tags) == set([t["name"] for t in data["tags"]])
393
+ assert dataset.harvest.remote_id == result["result"]["id"]
394
+ assert dataset.harvest.domain == "localhost"
395
+ assert dataset.harvest.ckan_name == data["name"]
396
+ assert len(dataset.resources) == 1
397
+
398
+ resource = dataset.resources[0]
399
+ assert resource.title == resource_data["name"]
400
+ assert resource.description == resource_data["description"]
401
+ assert resource.url == resource_data["url"]
402
+ # Use result because format is normalized by CKAN
403
+ assert resource.format == resource_result["format"].lower()
404
+ assert resource.mime == resource_data["mimetype"].lower()
405
+ assert resource.harvest.issued_at.date() == date(2022, 9, 29)
406
+ assert resource.harvest.modified_at.date() == date(2022, 9, 30)
407
+
408
+ @pytest.mark.ckan_data("spatial_geom_polygon")
409
+ def test_geospatial_geom_polygon(self, result, kwargs):
410
+ polygon = kwargs["polygon"]
411
+ dataset = dataset_for(result)
412
+
413
+ assert dataset.spatial.geom == {
414
+ "type": "MultiPolygon",
415
+ "coordinates": [polygon["coordinates"]],
416
+ }
526
417
 
527
- @pytest.mark.ckan_data("unknown_spatial_text")
528
- def test_keep_unknown_spatial_text_as_extra(result, kwargs):
529
- dataset = dataset_for(result)
530
- assert dataset.extras["ckan:spatial-text"] == kwargs["spatial"]
531
- assert dataset.spatial is None
418
+ @pytest.mark.ckan_data("spatial_geom_multipolygon")
419
+ def test_geospatial_geom_multipolygon(self, result, kwargs):
420
+ multipolygon = kwargs["multipolygon"]
532
421
 
422
+ dataset = dataset_for(result)
423
+ assert dataset.spatial.geom == multipolygon
533
424
 
534
- @pytest.mark.ckan_data("spatial_uri")
535
- def test_keep_unknown_spatial_uri_as_extra(result, kwargs):
536
- dataset = dataset_for(result)
537
- assert dataset.extras["ckan:spatial-uri"] == kwargs["spatial"]
538
- assert dataset.spatial is None
425
+ @pytest.mark.ckan_data("skipped_no_resources")
426
+ def test_skip_no_resources(self, source, result):
427
+ job = source.get_last_job()
428
+ item = job_item_for(job, result)
429
+ assert item.status == "skipped"
430
+ assert dataset_for(result) is None
431
+
432
+ @pytest.mark.ckan_data("ckan_url_is_url")
433
+ def test_ckan_url_is_url(self, data, result):
434
+ dataset = dataset_for(result)
435
+ assert dataset.harvest.remote_url == data["url"]
436
+ assert dataset.harvest.ckan_source is None
437
+
438
+ @pytest.mark.ckan_data("ckan_url_is_a_string")
439
+ def test_ckan_url_is_string(self, ckan, data, result):
440
+ dataset = dataset_for(result)
441
+ expected_url = "{0}/dataset/{1}".format(ckan.BASE_URL, data["name"])
442
+ assert dataset.harvest.remote_url == expected_url
443
+ assert dataset.harvest.ckan_source == data["url"]
444
+
445
+ @pytest.mark.ckan_data("frequency_as_rdf_uri")
446
+ def test_can_parse_frequency_as_uri(self, result, kwargs):
447
+ dataset = dataset_for(result)
448
+ assert dataset.frequency == kwargs["expected"]
449
+ assert "ckan:frequency" not in dataset.extras
450
+
451
+ @pytest.mark.ckan_data("frequency_as_exact_match")
452
+ def test_can_parse_frequency_as_exact_match(self, result, kwargs):
453
+ dataset = dataset_for(result)
454
+ assert dataset.frequency == kwargs["expected"]
455
+ assert "ckan:frequency" not in dataset.extras
456
+
457
+ @pytest.mark.ckan_data("frequency_as_unknown_value")
458
+ def test_can_parse_frequency_as_unknown_value(self, result, kwargs):
459
+ dataset = dataset_for(result)
460
+ assert dataset.extras["ckan:frequency"] == kwargs["expected"]
461
+ assert dataset.frequency is None
462
+
463
+ @pytest.mark.ckan_data("empty_extras")
464
+ def test_skip_empty_extras(self, result):
465
+ dataset = dataset_for(result)
466
+ assert "none" not in dataset.extras
467
+ assert "blank" not in dataset.extras
468
+ assert "spaces" not in dataset.extras
469
+
470
+ @pytest.mark.ckan_data("known_spatial_text_name")
471
+ def test_known_spatial_text_name(self, result, kwargs):
472
+ zone = kwargs["zone"]
473
+ dataset = dataset_for(result)
474
+ assert zone in dataset.spatial.zones
475
+ assert "ckan:spatial-text" not in dataset.extras
476
+
477
+ @pytest.mark.ckan_data("known_spatial_text_slug")
478
+ def test_known_spatial_text_slug(self, result, kwargs):
479
+ zone = kwargs["zone"]
480
+ dataset = dataset_for(result)
481
+ assert zone in dataset.spatial.zones
482
+ assert "ckan:spatial-text" not in dataset.extras
483
+
484
+ @pytest.mark.ckan_data("multiple_known_spatial_text")
485
+ def test_store_unsure_spatial_text_as_extra(self, result, kwargs):
486
+ dataset = dataset_for(result)
487
+ assert dataset.extras["ckan:spatial-text"] == kwargs["name"]
488
+ assert dataset.spatial is None
489
+
490
+ @pytest.mark.ckan_data("unknown_spatial_text")
491
+ def test_keep_unknown_spatial_text_as_extra(self, result, kwargs):
492
+ dataset = dataset_for(result)
493
+ assert dataset.extras["ckan:spatial-text"] == kwargs["spatial"]
494
+ assert dataset.spatial is None
495
+
496
+ @pytest.mark.ckan_data("spatial_uri")
497
+ def test_keep_unknown_spatial_uri_as_extra(self, result, kwargs):
498
+ dataset = dataset_for(result)
499
+ assert dataset.extras["ckan:spatial-uri"] == kwargs["spatial"]
500
+ assert dataset.spatial is None
539
501
 
540
502
 
541
503
  ##############################################################################
542
504
  # Edge cases manually written #
543
505
  ##############################################################################
544
- def test_minimal_ckan_response(app, rmock):
545
- """CKAN Harvester should accept the minimum dataset payload"""
546
- CKAN_URL = "https://harvest.me/"
547
- API_URL = "{}api/3/action/".format(CKAN_URL)
548
- PACKAGE_LIST_URL = "{}package_list".format(API_URL)
549
- PACKAGE_SHOW_URL = "{}package_show".format(API_URL)
550
-
551
- name = faker.unique_string()
552
- json = {
553
- "success": True,
554
- "result": minimal_data(name=name),
555
- }
556
- source = HarvestSourceFactory(backend="ckan", url=CKAN_URL)
557
- rmock.get(
558
- PACKAGE_LIST_URL,
559
- json={"success": True, "result": [name]},
560
- status_code=200,
561
- headers={"Content-Type": "application/json"},
562
- )
563
- rmock.get(
564
- PACKAGE_SHOW_URL,
565
- json=json,
566
- status_code=200,
567
- headers={"Content-Type": "application/json"},
568
- )
569
- actions.run(source)
570
- source.reload()
571
- assert source.get_last_job().status == "done"
572
-
573
-
574
- def test_flawed_ckan_response(app, rmock):
575
- """CKAN Harvester should report item error with id == remote_id in item"""
576
- CKAN_URL = "https://harvest.me/"
577
- API_URL = "{}api/3/action/".format(CKAN_URL)
578
- PACKAGE_LIST_URL = "{}package_list".format(API_URL)
579
- PACKAGE_SHOW_URL = "{}package_show".format(API_URL)
580
-
581
- name = faker.unique_string()
582
- _id = faker.uuid4()
583
- # flawed response, missing way too much required attrs
584
- json = {
585
- "success": True,
586
- "result": {
587
- "id": _id,
588
- "name": name,
589
- },
590
- }
591
- source = HarvestSourceFactory(backend="ckan", url=CKAN_URL)
592
- rmock.get(
593
- PACKAGE_LIST_URL,
594
- json={"success": True, "result": [name]},
595
- status_code=200,
596
- headers={"Content-Type": "application/json"},
597
- )
598
- rmock.get(
599
- PACKAGE_SHOW_URL,
600
- json=json,
601
- status_code=200,
602
- headers={"Content-Type": "application/json"},
603
- )
604
- actions.run(source)
605
- source.reload()
606
- assert source.get_last_job().status == "done-errors"
607
- assert source.get_last_job().items[0].remote_id == _id
608
- # flawed response, without an id, we should fallback on the name
609
- json = {
610
- "success": True,
611
- "result": {
612
- "name": name,
613
- },
614
- }
615
- rmock.get(
616
- PACKAGE_SHOW_URL,
617
- json=json,
618
- status_code=200,
619
- headers={"Content-Type": "application/json"},
620
- )
621
- actions.run(source)
622
- source.reload()
623
- assert source.get_last_job().status == "done-errors"
624
- assert source.get_last_job().items[0].remote_id == name
625
-
626
-
627
- @pytest.mark.options(HARVEST_MAX_ITEMS=1)
628
- def test_max_items(app, rmock):
629
- """CKAN Harvester should report item error with id == remote_id in item"""
630
- CKAN_URL = "https://harvest.me/"
631
- API_URL = "{}api/3/action/".format(CKAN_URL)
632
- PACKAGE_LIST_URL = "{}package_list".format(API_URL)
633
- PACKAGE_SHOW_URL = "{}package_show".format(API_URL)
634
-
635
- name_a = faker.unique_string()
636
- name_b = faker.unique_string()
637
- id_a = faker.uuid4()
638
- json_a = {
639
- "success": True,
640
- "result": minimal_data(id=id_a, name=name_a),
641
- }
642
- id_b = faker.uuid4()
643
- json_b = {
644
- "success": True,
645
- "result": minimal_data(id=id_b, name=name_b),
646
- }
647
- source = HarvestSourceFactory(backend="ckan", url=CKAN_URL)
648
- rmock.get(
649
- PACKAGE_LIST_URL,
650
- json={"success": True, "result": [name_a, name_b]},
651
- status_code=200,
652
- headers={"Content-Type": "application/json"},
653
- )
654
- rmock.get(
655
- f"{PACKAGE_SHOW_URL}?id={name_a}",
656
- json=json_a,
657
- status_code=200,
658
- headers={"Content-Type": "application/json"},
659
- )
660
- rmock.get(
661
- f"{PACKAGE_SHOW_URL}?id={name_b}",
662
- json=json_b,
663
- status_code=200,
664
- headers={"Content-Type": "application/json"},
665
- )
666
- actions.run(source)
667
- source.reload()
668
- assert source.get_last_job().status == "done"
669
- assert len(source.get_last_job().items) == 1
670
- assert source.get_last_job().items[0].remote_id == id_a
671
506
 
672
507
 
673
- def minimal_data(**kwargs):
674
- # extras and revision_id are not always present so we exclude them
675
- # from the minimal payload
676
- return {
677
- **{
678
- "id": faker.uuid4(),
679
- "name": faker.uuid4(),
680
- "title": faker.sentence(),
681
- "maintainer": faker.name(),
682
- "tags": [],
683
- "private": False,
684
- "maintainer_email": faker.email(),
685
- "license_id": None,
686
- "metadata_created": faker.iso8601(),
687
- "organization": None,
688
- "metadata_modified": faker.iso8601(),
689
- "author": None,
690
- "author_email": None,
691
- "notes": faker.paragraph(),
692
- "license_title": None,
693
- "state": None,
694
- "type": "dataset",
695
- "resources": [],
696
- },
697
- **kwargs,
698
- }
508
+ @pytest.mark.options(HARVESTER_BACKENDS=["ckan"])
509
+ class CkanBackendEdgeCasesTest(PytestOnlyDBTestCase):
510
+ def test_minimal_ckan_response(self, rmock):
511
+ """CKAN Harvester should accept the minimum dataset payload"""
512
+ CKAN_URL = "https://harvest.me/"
513
+ API_URL = "{}api/3/action/".format(CKAN_URL)
514
+ PACKAGE_LIST_URL = "{}package_list".format(API_URL)
515
+ PACKAGE_SHOW_URL = "{}package_show".format(API_URL)
516
+
517
+ name = faker.unique_string()
518
+ json = {
519
+ "success": True,
520
+ "result": self.minimal_data(name=name),
521
+ }
522
+ source = HarvestSourceFactory(backend="ckan", url=CKAN_URL)
523
+ rmock.get(
524
+ PACKAGE_LIST_URL,
525
+ json={"success": True, "result": [name]},
526
+ status_code=200,
527
+ headers={"Content-Type": "application/json"},
528
+ )
529
+ rmock.get(
530
+ PACKAGE_SHOW_URL,
531
+ json=json,
532
+ status_code=200,
533
+ headers={"Content-Type": "application/json"},
534
+ )
535
+ actions.run(source)
536
+ source.reload()
537
+ assert source.get_last_job().status == "done"
538
+
539
+ def test_flawed_ckan_response(self, rmock):
540
+ """CKAN Harvester should report item error with id == remote_id in item"""
541
+ CKAN_URL = "https://harvest.me/"
542
+ API_URL = "{}api/3/action/".format(CKAN_URL)
543
+ PACKAGE_LIST_URL = "{}package_list".format(API_URL)
544
+ PACKAGE_SHOW_URL = "{}package_show".format(API_URL)
545
+
546
+ name = faker.unique_string()
547
+ _id = faker.uuid4()
548
+ # flawed response, missing way too much required attrs
549
+ json = {
550
+ "success": True,
551
+ "result": {
552
+ "id": _id,
553
+ "name": name,
554
+ },
555
+ }
556
+ source = HarvestSourceFactory(backend="ckan", url=CKAN_URL)
557
+ rmock.get(
558
+ PACKAGE_LIST_URL,
559
+ json={"success": True, "result": [name]},
560
+ status_code=200,
561
+ headers={"Content-Type": "application/json"},
562
+ )
563
+ rmock.get(
564
+ PACKAGE_SHOW_URL,
565
+ json=json,
566
+ status_code=200,
567
+ headers={"Content-Type": "application/json"},
568
+ )
569
+ actions.run(source)
570
+ source.reload()
571
+ assert source.get_last_job().status == "done-errors"
572
+ assert source.get_last_job().items[0].remote_id == _id
573
+ # flawed response, without an id, we should fallback on the name
574
+ json = {
575
+ "success": True,
576
+ "result": {
577
+ "name": name,
578
+ },
579
+ }
580
+ rmock.get(
581
+ PACKAGE_SHOW_URL,
582
+ json=json,
583
+ status_code=200,
584
+ headers={"Content-Type": "application/json"},
585
+ )
586
+ actions.run(source)
587
+ source.reload()
588
+ assert source.get_last_job().status == "done-errors"
589
+ assert source.get_last_job().items[0].remote_id == name
590
+
591
+ @pytest.mark.options(HARVEST_MAX_ITEMS=1)
592
+ def test_max_items(self, app, rmock):
593
+ """CKAN Harvester should report item error with id == remote_id in item"""
594
+ CKAN_URL = "https://harvest.me/"
595
+ API_URL = "{}api/3/action/".format(CKAN_URL)
596
+ PACKAGE_LIST_URL = "{}package_list".format(API_URL)
597
+ PACKAGE_SHOW_URL = "{}package_show".format(API_URL)
598
+
599
+ name_a = faker.unique_string()
600
+ name_b = faker.unique_string()
601
+ id_a = faker.uuid4()
602
+ json_a = {
603
+ "success": True,
604
+ "result": self.minimal_data(id=id_a, name=name_a),
605
+ }
606
+ id_b = faker.uuid4()
607
+ json_b = {
608
+ "success": True,
609
+ "result": self.minimal_data(id=id_b, name=name_b),
610
+ }
611
+ source = HarvestSourceFactory(backend="ckan", url=CKAN_URL)
612
+ rmock.get(
613
+ PACKAGE_LIST_URL,
614
+ json={"success": True, "result": [name_a, name_b]},
615
+ status_code=200,
616
+ headers={"Content-Type": "application/json"},
617
+ )
618
+ rmock.get(
619
+ f"{PACKAGE_SHOW_URL}?id={name_a}",
620
+ json=json_a,
621
+ status_code=200,
622
+ headers={"Content-Type": "application/json"},
623
+ )
624
+ rmock.get(
625
+ f"{PACKAGE_SHOW_URL}?id={name_b}",
626
+ json=json_b,
627
+ status_code=200,
628
+ headers={"Content-Type": "application/json"},
629
+ )
630
+ actions.run(source)
631
+ source.reload()
632
+ assert source.get_last_job().status == "done"
633
+ assert len(source.get_last_job().items) == 1
634
+ assert source.get_last_job().items[0].remote_id == id_a
635
+
636
+ def minimal_data(self, **kwargs):
637
+ # extras and revision_id are not always present so we exclude them
638
+ # from the minimal payload
639
+ return {
640
+ **{
641
+ "id": faker.uuid4(),
642
+ "name": faker.uuid4(),
643
+ "title": faker.sentence(),
644
+ "maintainer": faker.name(),
645
+ "tags": [],
646
+ "private": False,
647
+ "maintainer_email": faker.email(),
648
+ "license_id": None,
649
+ "metadata_created": faker.iso8601(),
650
+ "organization": None,
651
+ "metadata_modified": faker.iso8601(),
652
+ "author": None,
653
+ "author_email": None,
654
+ "notes": faker.paragraph(),
655
+ "license_title": None,
656
+ "state": None,
657
+ "type": "dataset",
658
+ "resources": [],
659
+ },
660
+ **kwargs,
661
+ }