udata 14.0.0__py3-none-any.whl → 14.4.1.dev7__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 (130) hide show
  1. udata/api_fields.py +35 -4
  2. udata/app.py +18 -20
  3. udata/auth/__init__.py +29 -6
  4. udata/auth/forms.py +2 -2
  5. udata/auth/views.py +6 -3
  6. udata/commands/serve.py +3 -11
  7. udata/commands/tests/test_fixtures.py +9 -9
  8. udata/core/access_type/api.py +1 -1
  9. udata/core/access_type/constants.py +12 -8
  10. udata/core/activity/api.py +5 -6
  11. udata/core/badges/tests/test_commands.py +6 -6
  12. udata/core/csv.py +5 -0
  13. udata/core/dataservices/models.py +1 -1
  14. udata/core/dataservices/tasks.py +7 -0
  15. udata/core/dataset/api.py +2 -0
  16. udata/core/dataset/models.py +2 -2
  17. udata/core/dataset/permissions.py +31 -0
  18. udata/core/dataset/tasks.py +17 -5
  19. udata/core/discussions/models.py +1 -0
  20. udata/core/organization/api.py +8 -5
  21. udata/core/organization/mails.py +1 -1
  22. udata/core/organization/models.py +9 -1
  23. udata/core/organization/notifications.py +84 -0
  24. udata/core/organization/permissions.py +1 -1
  25. udata/core/organization/tasks.py +3 -0
  26. udata/core/pages/tests/test_api.py +32 -0
  27. udata/core/post/api.py +24 -69
  28. udata/core/post/models.py +84 -16
  29. udata/core/post/tests/test_api.py +24 -1
  30. udata/core/reports/api.py +18 -0
  31. udata/core/reports/models.py +42 -2
  32. udata/core/reuse/models.py +1 -1
  33. udata/core/reuse/tasks.py +7 -0
  34. udata/core/spatial/forms.py +2 -2
  35. udata/core/user/models.py +5 -1
  36. udata/features/notifications/api.py +7 -18
  37. udata/features/notifications/models.py +56 -0
  38. udata/features/notifications/tasks.py +25 -0
  39. udata/flask_mongoengine/engine.py +0 -4
  40. udata/frontend/markdown.py +2 -1
  41. udata/harvest/actions.py +21 -1
  42. udata/harvest/api.py +25 -8
  43. udata/harvest/backends/base.py +27 -1
  44. udata/harvest/backends/ckan/harvesters.py +11 -2
  45. udata/harvest/commands.py +33 -0
  46. udata/harvest/filters.py +17 -6
  47. udata/harvest/models.py +16 -0
  48. udata/harvest/permissions.py +27 -0
  49. udata/harvest/tests/ckan/test_ckan_backend.py +33 -0
  50. udata/harvest/tests/test_actions.py +58 -5
  51. udata/harvest/tests/test_api.py +276 -122
  52. udata/harvest/tests/test_base_backend.py +86 -1
  53. udata/harvest/tests/test_dcat_backend.py +57 -10
  54. udata/harvest/tests/test_filters.py +6 -0
  55. udata/i18n.py +1 -4
  56. udata/mail.py +5 -1
  57. udata/migrations/2025-10-31-create-membership-request-notifications.py +55 -0
  58. udata/migrations/2025-12-04-add-uuid-to-discussion-messages.py +28 -0
  59. udata/mongo/slug_fields.py +1 -1
  60. udata/rdf.py +45 -6
  61. udata/routing.py +2 -2
  62. udata/settings.py +7 -0
  63. udata/tasks.py +1 -0
  64. udata/templates/mail/message.html +5 -31
  65. udata/tests/__init__.py +27 -2
  66. udata/tests/api/__init__.py +108 -21
  67. udata/tests/api/test_activities_api.py +36 -0
  68. udata/tests/api/test_auth_api.py +121 -95
  69. udata/tests/api/test_base_api.py +7 -4
  70. udata/tests/api/test_datasets_api.py +44 -19
  71. udata/tests/api/test_organizations_api.py +192 -197
  72. udata/tests/api/test_reports_api.py +157 -0
  73. udata/tests/api/test_reuses_api.py +147 -147
  74. udata/tests/api/test_security_api.py +12 -12
  75. udata/tests/api/test_swagger.py +4 -4
  76. udata/tests/api/test_tags_api.py +8 -8
  77. udata/tests/api/test_user_api.py +1 -1
  78. udata/tests/apiv2/test_swagger.py +4 -4
  79. udata/tests/cli/test_cli_base.py +8 -9
  80. udata/tests/dataset/test_dataset_commands.py +4 -4
  81. udata/tests/dataset/test_dataset_model.py +66 -26
  82. udata/tests/dataset/test_dataset_rdf.py +99 -5
  83. udata/tests/frontend/test_auth.py +24 -1
  84. udata/tests/frontend/test_csv.py +0 -3
  85. udata/tests/helpers.py +25 -27
  86. udata/tests/organization/test_notifications.py +67 -2
  87. udata/tests/plugin.py +6 -261
  88. udata/tests/site/test_site_csv_exports.py +22 -10
  89. udata/tests/test_activity.py +9 -9
  90. udata/tests/test_dcat_commands.py +2 -2
  91. udata/tests/test_discussions.py +5 -5
  92. udata/tests/test_migrations.py +21 -21
  93. udata/tests/test_notifications.py +15 -57
  94. udata/tests/test_notifications_task.py +43 -0
  95. udata/tests/test_owned.py +81 -1
  96. udata/tests/test_storages.py +25 -19
  97. udata/tests/test_topics.py +77 -61
  98. udata/tests/test_uris.py +33 -0
  99. udata/tests/workers/test_jobs_commands.py +23 -23
  100. udata/translations/ar/LC_MESSAGES/udata.mo +0 -0
  101. udata/translations/ar/LC_MESSAGES/udata.po +187 -108
  102. udata/translations/de/LC_MESSAGES/udata.mo +0 -0
  103. udata/translations/de/LC_MESSAGES/udata.po +187 -108
  104. udata/translations/es/LC_MESSAGES/udata.mo +0 -0
  105. udata/translations/es/LC_MESSAGES/udata.po +187 -108
  106. udata/translations/fr/LC_MESSAGES/udata.mo +0 -0
  107. udata/translations/fr/LC_MESSAGES/udata.po +188 -109
  108. udata/translations/it/LC_MESSAGES/udata.mo +0 -0
  109. udata/translations/it/LC_MESSAGES/udata.po +187 -108
  110. udata/translations/pt/LC_MESSAGES/udata.mo +0 -0
  111. udata/translations/pt/LC_MESSAGES/udata.po +187 -108
  112. udata/translations/sr/LC_MESSAGES/udata.mo +0 -0
  113. udata/translations/sr/LC_MESSAGES/udata.po +187 -108
  114. udata/translations/udata.pot +215 -106
  115. udata/uris.py +0 -2
  116. udata-14.4.1.dev7.dist-info/METADATA +109 -0
  117. {udata-14.0.0.dist-info → udata-14.4.1.dev7.dist-info}/RECORD +121 -123
  118. udata/core/post/forms.py +0 -30
  119. udata/flask_mongoengine/json.py +0 -38
  120. udata/templates/mail/base.html +0 -105
  121. udata/templates/mail/base.txt +0 -6
  122. udata/templates/mail/button.html +0 -3
  123. udata/templates/mail/layouts/1-column.html +0 -19
  124. udata/templates/mail/layouts/2-columns.html +0 -20
  125. udata/templates/mail/layouts/center-panel.html +0 -16
  126. udata-14.0.0.dist-info/METADATA +0 -132
  127. {udata-14.0.0.dist-info → udata-14.4.1.dev7.dist-info}/WHEEL +0 -0
  128. {udata-14.0.0.dist-info → udata-14.4.1.dev7.dist-info}/entry_points.txt +0 -0
  129. {udata-14.0.0.dist-info → udata-14.4.1.dev7.dist-info}/licenses/LICENSE +0 -0
  130. {udata-14.0.0.dist-info → udata-14.4.1.dev7.dist-info}/top_level.txt +0 -0
@@ -5,13 +5,14 @@ import pytest
5
5
  from flask import url_for
6
6
  from pytest_mock import MockerFixture
7
7
 
8
+ from udata.core.dataservices.factories import DataserviceFactory
9
+ from udata.core.dataset.factories import DatasetFactory
8
10
  from udata.core.organization.factories import OrganizationFactory
9
11
  from udata.core.user.factories import AdminFactory, UserFactory
10
12
  from udata.harvest.backends import get_enabled_backends
11
13
  from udata.models import Member, PeriodicTask
12
14
  from udata.tests.api import PytestOnlyAPITestCase
13
15
  from udata.tests.helpers import assert200, assert201, assert204, assert400, assert403, assert404
14
- from udata.tests.plugin import ApiClient
15
16
  from udata.utils import faker
16
17
 
17
18
  from .. import actions
@@ -19,18 +20,19 @@ from ..models import (
19
20
  VALIDATION_ACCEPTED,
20
21
  VALIDATION_PENDING,
21
22
  VALIDATION_REFUSED,
23
+ HarvestItem,
22
24
  HarvestSource,
23
25
  HarvestSourceValidation,
24
26
  )
25
- from .factories import HarvestSourceFactory, MockBackendsMixin
27
+ from .factories import HarvestJobFactory, HarvestSourceFactory, MockBackendsMixin
26
28
 
27
29
  log = logging.getLogger(__name__)
28
30
 
29
31
 
30
32
  class HarvestAPITest(MockBackendsMixin, PytestOnlyAPITestCase):
31
- def test_list_backends(self, api):
33
+ def test_list_backends(self):
32
34
  """It should fetch the harvest backends list from the API"""
33
- response = api.get(url_for("api.harvest_backends"))
35
+ response = self.get(url_for("api.harvest_backends"))
34
36
  assert200(response)
35
37
  assert len(response.json) == len(get_enabled_backends())
36
38
  for data in response.json:
@@ -40,87 +42,87 @@ class HarvestAPITest(MockBackendsMixin, PytestOnlyAPITestCase):
40
42
  assert isinstance(data["filters"], (list, tuple))
41
43
  assert "extra_configs" in data
42
44
 
43
- def test_list_sources(self, api):
45
+ def test_list_sources(self):
44
46
  sources = HarvestSourceFactory.create_batch(3)
45
47
 
46
- response = api.get(url_for("api.harvest_sources"))
48
+ response = self.get(url_for("api.harvest_sources"))
47
49
  assert200(response)
48
50
  assert len(response.json["data"]) == len(sources)
49
51
 
50
- def test_list_sources_exclude_deleted(self, api):
52
+ def test_list_sources_exclude_deleted(self):
51
53
  sources = HarvestSourceFactory.create_batch(3)
52
54
  HarvestSourceFactory.create_batch(2, deleted=datetime.utcnow())
53
55
 
54
- response = api.get(url_for("api.harvest_sources"))
56
+ response = self.get(url_for("api.harvest_sources"))
55
57
  assert200(response)
56
58
  assert len(response.json["data"]) == len(sources)
57
59
 
58
- def test_list_sources_include_deleted(self, api):
60
+ def test_list_sources_include_deleted(self):
59
61
  sources = HarvestSourceFactory.create_batch(3)
60
62
  sources.extend(HarvestSourceFactory.create_batch(2, deleted=datetime.utcnow()))
61
63
 
62
- response = api.get(url_for("api.harvest_sources", deleted=True))
64
+ response = self.get(url_for("api.harvest_sources", deleted=True))
63
65
  assert200(response)
64
66
  assert len(response.json["data"]) == len(sources)
65
67
 
66
- def test_list_sources_for_owner(self, api):
68
+ def test_list_sources_for_owner(self):
67
69
  owner = UserFactory()
68
70
  sources = HarvestSourceFactory.create_batch(3, owner=owner)
69
71
  HarvestSourceFactory()
70
72
 
71
73
  url = url_for("api.harvest_sources", owner=str(owner.id))
72
- response = api.get(url)
74
+ response = self.get(url)
73
75
  assert200(response)
74
76
 
75
77
  assert len(response.json["data"]) == len(sources)
76
78
 
77
- def test_list_sources_for_org(self, api):
79
+ def test_list_sources_for_org(self):
78
80
  org = OrganizationFactory()
79
81
  sources = HarvestSourceFactory.create_batch(3, organization=org)
80
82
  HarvestSourceFactory()
81
83
 
82
- response = api.get(url_for("api.harvest_sources", owner=str(org.id)))
84
+ response = self.get(url_for("api.harvest_sources", owner=str(org.id)))
83
85
  assert200(response)
84
86
 
85
87
  assert len(response.json["data"]) == len(sources)
86
88
 
87
- def test_list_sources_search(self, api):
89
+ def test_list_sources_search(self):
88
90
  HarvestSourceFactory.create_batch(3)
89
91
  source = HarvestSourceFactory(name="Moissonneur GeoNetwork de la ville de Rennes")
90
92
 
91
93
  url = url_for("api.harvest_sources", q="geonetwork rennes")
92
- response = api.get(url)
94
+ response = self.get(url)
93
95
  assert200(response)
94
96
 
95
97
  assert len(response.json["data"]) == 1
96
98
  assert response.json["data"][0]["id"] == str(source.id)
97
99
 
98
- def test_list_sources_paginate(self, api):
100
+ def test_list_sources_paginate(self):
99
101
  total = 25
100
102
  page_size = 20
101
103
  HarvestSourceFactory.create_batch(total)
102
104
 
103
105
  url = url_for("api.harvest_sources", page=1, page_size=page_size)
104
- response = api.get(url)
106
+ response = self.get(url)
105
107
  assert200(response)
106
108
  assert len(response.json["data"]) == page_size
107
109
  assert response.json["total"] == total
108
110
 
109
111
  url = url_for("api.harvest_sources", page=2, page_size=page_size)
110
- response = api.get(url)
112
+ response = self.get(url)
111
113
  assert200(response)
112
114
  assert len(response.json["data"]) == total - page_size
113
115
  assert response.json["total"] == total
114
116
 
115
117
  url = url_for("api.harvest_sources", page=3, page_size=page_size)
116
- response = api.get(url)
118
+ response = self.get(url)
117
119
  assert404(response)
118
120
 
119
- def test_create_source_with_owner(self, api):
121
+ def test_create_source_with_owner(self):
120
122
  """It should create and attach a new source to an owner"""
121
- user = api.login()
123
+ user = self.login()
122
124
  data = {"name": faker.word(), "url": faker.url(), "backend": "factory"}
123
- response = api.post(url_for("api.harvest_sources"), data)
125
+ response = self.post(url_for("api.harvest_sources"), data)
124
126
 
125
127
  assert201(response)
126
128
 
@@ -129,9 +131,9 @@ class HarvestAPITest(MockBackendsMixin, PytestOnlyAPITestCase):
129
131
  assert source["owner"]["id"] == str(user.id)
130
132
  assert source["organization"] is None
131
133
 
132
- def test_create_source_with_org(self, api):
134
+ def test_create_source_with_org(self):
133
135
  """It should create and attach a new source to an organization"""
134
- user = api.login()
136
+ user = self.login()
135
137
  member = Member(user=user, role="admin")
136
138
  org = OrganizationFactory(members=[member])
137
139
  data = {
@@ -140,7 +142,7 @@ class HarvestAPITest(MockBackendsMixin, PytestOnlyAPITestCase):
140
142
  "backend": "factory",
141
143
  "organization": str(org.id),
142
144
  }
143
- response = api.post(url_for("api.harvest_sources"), data)
145
+ response = self.post(url_for("api.harvest_sources"), data)
144
146
 
145
147
  assert201(response)
146
148
 
@@ -149,9 +151,9 @@ class HarvestAPITest(MockBackendsMixin, PytestOnlyAPITestCase):
149
151
  assert source["owner"] is None
150
152
  assert source["organization"]["id"] == str(org.id)
151
153
 
152
- def test_create_source_with_org_not_member(self, api):
154
+ def test_create_source_with_org_not_member(self):
153
155
  """It should create and attach a new source to an organization"""
154
- user = api.login()
156
+ user = self.login()
155
157
  member = Member(user=user, role="editor")
156
158
  org = OrganizationFactory(members=[member])
157
159
  data = {
@@ -160,13 +162,13 @@ class HarvestAPITest(MockBackendsMixin, PytestOnlyAPITestCase):
160
162
  "backend": "factory",
161
163
  "organization": str(org.id),
162
164
  }
163
- response = api.post(url_for("api.harvest_sources"), data)
165
+ response = self.post(url_for("api.harvest_sources"), data)
164
166
 
165
167
  assert403(response)
166
168
 
167
- def test_create_source_with_config(self, api):
169
+ def test_create_source_with_config(self):
168
170
  """It should create a new source with configuration"""
169
- api.login()
171
+ self.login()
170
172
  data = {
171
173
  "name": faker.word(),
172
174
  "url": faker.url(),
@@ -187,7 +189,7 @@ class HarvestAPITest(MockBackendsMixin, PytestOnlyAPITestCase):
187
189
  ],
188
190
  },
189
191
  }
190
- response = api.post(url_for("api.harvest_sources"), data)
192
+ response = self.post(url_for("api.harvest_sources"), data)
191
193
 
192
194
  assert201(response)
193
195
 
@@ -208,9 +210,9 @@ class HarvestAPITest(MockBackendsMixin, PytestOnlyAPITestCase):
208
210
  ],
209
211
  }
210
212
 
211
- def test_create_source_with_unknown_filter(self, api):
213
+ def test_create_source_with_unknown_filter(self):
212
214
  """Can only use known filters in config"""
213
- api.login()
215
+ self.login()
214
216
  data = {
215
217
  "name": faker.word(),
216
218
  "url": faker.url(),
@@ -221,13 +223,13 @@ class HarvestAPITest(MockBackendsMixin, PytestOnlyAPITestCase):
221
223
  ]
222
224
  },
223
225
  }
224
- response = api.post(url_for("api.harvest_sources"), data)
226
+ response = self.post(url_for("api.harvest_sources"), data)
225
227
 
226
228
  assert400(response)
227
229
 
228
- def test_create_source_with_bad_filter_type(self, api):
230
+ def test_create_source_with_bad_filter_type(self):
229
231
  """Can only use the xpected filter type"""
230
- api.login()
232
+ self.login()
231
233
  data = {
232
234
  "name": faker.word(),
233
235
  "url": faker.url(),
@@ -238,13 +240,13 @@ class HarvestAPITest(MockBackendsMixin, PytestOnlyAPITestCase):
238
240
  ]
239
241
  },
240
242
  }
241
- response = api.post(url_for("api.harvest_sources"), data)
243
+ response = self.post(url_for("api.harvest_sources"), data)
242
244
 
243
245
  assert400(response)
244
246
 
245
- def test_create_source_with_bad_filter_format(self, api):
247
+ def test_create_source_with_bad_filter_format(self):
246
248
  """Filters should have the right format"""
247
- api.login()
249
+ self.login()
248
250
  data = {
249
251
  "name": faker.word(),
250
252
  "url": faker.url(),
@@ -255,13 +257,13 @@ class HarvestAPITest(MockBackendsMixin, PytestOnlyAPITestCase):
255
257
  ]
256
258
  },
257
259
  }
258
- response = api.post(url_for("api.harvest_sources"), data)
260
+ response = self.post(url_for("api.harvest_sources"), data)
259
261
 
260
262
  assert400(response)
261
263
 
262
- def test_create_source_with_unknown_extra_config(self, api):
264
+ def test_create_source_with_unknown_extra_config(self):
263
265
  """Can only use known extra config in config"""
264
- api.login()
266
+ self.login()
265
267
  data = {
266
268
  "name": faker.word(),
267
269
  "url": faker.url(),
@@ -272,13 +274,13 @@ class HarvestAPITest(MockBackendsMixin, PytestOnlyAPITestCase):
272
274
  ]
273
275
  },
274
276
  }
275
- response = api.post(url_for("api.harvest_sources"), data)
277
+ response = self.post(url_for("api.harvest_sources"), data)
276
278
 
277
279
  assert400(response)
278
280
 
279
- def test_create_source_with_bad_extra_config_type(self, api):
281
+ def test_create_source_with_bad_extra_config_type(self):
280
282
  """Can only use the expected extra config type"""
281
- api.login()
283
+ self.login()
282
284
  data = {
283
285
  "name": faker.word(),
284
286
  "url": faker.url(),
@@ -289,13 +291,13 @@ class HarvestAPITest(MockBackendsMixin, PytestOnlyAPITestCase):
289
291
  ]
290
292
  },
291
293
  }
292
- response = api.post(url_for("api.harvest_sources"), data)
294
+ response = self.post(url_for("api.harvest_sources"), data)
293
295
 
294
296
  assert400(response)
295
297
 
296
- def test_create_source_with_bad_extra_config_format(self, api):
298
+ def test_create_source_with_bad_extra_config_format(self):
297
299
  """Extra config should have the right format"""
298
- api.login()
300
+ self.login()
299
301
  data = {
300
302
  "name": faker.word(),
301
303
  "url": faker.url(),
@@ -306,13 +308,13 @@ class HarvestAPITest(MockBackendsMixin, PytestOnlyAPITestCase):
306
308
  ]
307
309
  },
308
310
  }
309
- response = api.post(url_for("api.harvest_sources"), data)
311
+ response = self.post(url_for("api.harvest_sources"), data)
310
312
 
311
313
  assert400(response)
312
314
 
313
- def test_create_source_with_unknown_feature(self, api):
315
+ def test_create_source_with_unknown_feature(self):
314
316
  """Can only use known features in config"""
315
- api.login()
317
+ self.login()
316
318
  data = {
317
319
  "name": faker.word(),
318
320
  "url": faker.url(),
@@ -321,13 +323,13 @@ class HarvestAPITest(MockBackendsMixin, PytestOnlyAPITestCase):
321
323
  "features": {"unknown": True},
322
324
  },
323
325
  }
324
- response = api.post(url_for("api.harvest_sources"), data)
326
+ response = self.post(url_for("api.harvest_sources"), data)
325
327
 
326
328
  assert400(response)
327
329
 
328
- def test_create_source_with_false_feature(self, api):
330
+ def test_create_source_with_false_feature(self):
329
331
  """It should handled negative values"""
330
- api.login()
332
+ self.login()
331
333
  data = {
332
334
  "name": faker.word(),
333
335
  "url": faker.url(),
@@ -339,7 +341,7 @@ class HarvestAPITest(MockBackendsMixin, PytestOnlyAPITestCase):
339
341
  }
340
342
  },
341
343
  }
342
- response = api.post(url_for("api.harvest_sources"), data)
344
+ response = self.post(url_for("api.harvest_sources"), data)
343
345
 
344
346
  assert201(response)
345
347
 
@@ -351,9 +353,9 @@ class HarvestAPITest(MockBackendsMixin, PytestOnlyAPITestCase):
351
353
  }
352
354
  }
353
355
 
354
- def test_create_source_with_not_boolean_feature(self, api):
356
+ def test_create_source_with_not_boolean_feature(self):
355
357
  """It should handled negative values"""
356
- api.login()
358
+ self.login()
357
359
  data = {
358
360
  "name": faker.word(),
359
361
  "url": faker.url(),
@@ -364,28 +366,28 @@ class HarvestAPITest(MockBackendsMixin, PytestOnlyAPITestCase):
364
366
  }
365
367
  },
366
368
  }
367
- response = api.post(url_for("api.harvest_sources"), data)
369
+ response = self.post(url_for("api.harvest_sources"), data)
368
370
 
369
371
  assert400(response)
370
372
 
371
- def test_create_source_with_config_with_custom_key(self, api):
372
- api.login()
373
+ def test_create_source_with_config_with_custom_key(self):
374
+ self.login()
373
375
  data = {
374
376
  "name": faker.word(),
375
377
  "url": faker.url(),
376
378
  "backend": "factory",
377
379
  "config": {"custom": "value"},
378
380
  }
379
- response = api.post(url_for("api.harvest_sources"), data)
381
+ response = self.post(url_for("api.harvest_sources"), data)
380
382
 
381
383
  assert201(response)
382
384
 
383
385
  source = response.json
384
386
  assert source["config"] == {"custom": "value"}
385
387
 
386
- def test_update_source(self, api):
387
- """It should update a source if owner or orga member"""
388
- user = api.login()
388
+ def test_update_source(self):
389
+ """It should update a source if owner or orga admin"""
390
+ user = self.login()
389
391
  source = HarvestSourceFactory(owner=user)
390
392
  new_url = faker.url()
391
393
  data = {
@@ -395,20 +397,20 @@ class HarvestAPITest(MockBackendsMixin, PytestOnlyAPITestCase):
395
397
  "backend": "factory",
396
398
  }
397
399
  api_url = url_for("api.harvest_source", source=source)
398
- response = api.put(api_url, data)
400
+ response = self.put(api_url, data)
399
401
  assert200(response)
400
402
  assert response.json["url"] == new_url
401
403
 
402
- # Source is now owned by orga, with user as member
403
- source.organization = OrganizationFactory(members=[Member(user=user)])
404
+ # Source is now owned by orga, with user as admin
405
+ source.organization = OrganizationFactory(members=[Member(user=user, role="admin")])
404
406
  source.save()
405
407
  api_url = url_for("api.harvest_source", source=source)
406
- response = api.put(api_url, data)
408
+ response = self.put(api_url, data)
407
409
  assert200(response)
408
410
 
409
- def test_update_source_require_permission(self, api):
411
+ def test_update_source_require_permission(self):
410
412
  """It should not update a source if not the owner"""
411
- api.login()
413
+ self.login()
412
414
  source = HarvestSourceFactory()
413
415
  new_url: str = faker.url()
414
416
  data = {
@@ -418,32 +420,32 @@ class HarvestAPITest(MockBackendsMixin, PytestOnlyAPITestCase):
418
420
  "backend": "factory",
419
421
  }
420
422
  api_url: str = url_for("api.harvest_source", source=source)
421
- response = api.put(api_url, data)
423
+ response = self.put(api_url, data)
422
424
 
423
425
  assert403(response)
424
426
 
425
- def test_validate_source(self, api):
427
+ def test_validate_source(self):
426
428
  """It should allow to validate a source if admin"""
427
- user = api.login(AdminFactory())
429
+ user = self.login(AdminFactory())
428
430
  source = HarvestSourceFactory()
429
431
 
430
432
  data = {"state": VALIDATION_ACCEPTED}
431
433
  url = url_for("api.validate_harvest_source", source=source)
432
- response = api.post(url, data)
434
+ response = self.post(url, data)
433
435
  assert200(response)
434
436
 
435
437
  source.reload()
436
438
  assert source.validation.state == VALIDATION_ACCEPTED
437
439
  assert source.validation.by == user
438
440
 
439
- def test_reject_source(self, api):
441
+ def test_reject_source(self):
440
442
  """It should allow to reject a source if admin"""
441
- user = api.login(AdminFactory())
443
+ user = self.login(AdminFactory())
442
444
  source = HarvestSourceFactory()
443
445
 
444
446
  data = {"state": VALIDATION_REFUSED, "comment": "Not valid"}
445
447
  url = url_for("api.validate_harvest_source", source=source)
446
- response = api.post(url, data)
448
+ response = self.post(url, data)
447
449
  assert200(response)
448
450
 
449
451
  source.reload()
@@ -451,40 +453,40 @@ class HarvestAPITest(MockBackendsMixin, PytestOnlyAPITestCase):
451
453
  assert source.validation.comment == "Not valid"
452
454
  assert source.validation.by == user
453
455
 
454
- def test_validate_source_is_admin_only(self, api):
456
+ def test_validate_source_is_admin_only(self):
455
457
  """It should allow to validate a source if admin"""
456
- api.login()
458
+ self.login()
457
459
  source = HarvestSourceFactory()
458
460
 
459
461
  data = {"validate": True}
460
462
  url = url_for("api.validate_harvest_source", source=source)
461
- response = api.post(url, data)
463
+ response = self.post(url, data)
462
464
  assert403(response)
463
465
 
464
- def test_get_source(self, api):
466
+ def test_get_source(self):
465
467
  source = HarvestSourceFactory()
466
468
 
467
469
  url = url_for("api.harvest_source", source=source)
468
- response = api.get(url)
470
+ response = self.get(url)
469
471
  assert200(response)
470
472
 
471
- def test_get_missing_source(self, api):
473
+ def test_get_missing_source(self):
472
474
  url = url_for("api.harvest_source", source="685bb38b9cb9284b93fd9e72")
473
- response = api.get(url)
475
+ response = self.get(url)
474
476
  assert404(response)
475
477
 
476
- def test_source_preview(self, api):
477
- api.login()
478
- source = HarvestSourceFactory(backend="factory")
478
+ def test_source_preview(self):
479
+ user = self.login()
480
+ source = HarvestSourceFactory(backend="factory", owner=user)
479
481
 
480
482
  url = url_for("api.preview_harvest_source", source=source)
481
- response = api.get(url)
483
+ response = self.get(url)
482
484
  assert200(response)
483
485
 
484
486
  @pytest.mark.options(HARVEST_ENABLE_MANUAL_RUN=True)
485
- def test_run_source(self, mocker: MockerFixture, api: ApiClient):
487
+ def test_run_source(self, mocker: MockerFixture):
486
488
  launch = mocker.patch.object(actions.harvest, "delay")
487
- user = api.login()
489
+ user = self.login()
488
490
 
489
491
  source = HarvestSourceFactory(
490
492
  backend="factory",
@@ -493,15 +495,15 @@ class HarvestAPITest(MockBackendsMixin, PytestOnlyAPITestCase):
493
495
  )
494
496
 
495
497
  url = url_for("api.run_harvest_source", source=source)
496
- response = api.post(url)
498
+ response = self.post(url)
497
499
  assert200(response)
498
500
 
499
501
  launch.assert_called()
500
502
 
501
503
  @pytest.mark.options(HARVEST_ENABLE_MANUAL_RUN=False)
502
- def test_cannot_run_source_if_disabled(self, mocker: MockerFixture, api: ApiClient):
504
+ def test_cannot_run_source_if_disabled(self, mocker: MockerFixture):
503
505
  launch = mocker.patch.object(actions.harvest, "delay")
504
- user = api.login()
506
+ user = self.login()
505
507
 
506
508
  source = HarvestSourceFactory(
507
509
  backend="factory",
@@ -510,16 +512,16 @@ class HarvestAPITest(MockBackendsMixin, PytestOnlyAPITestCase):
510
512
  )
511
513
 
512
514
  url = url_for("api.run_harvest_source", source=source)
513
- response = api.post(url)
515
+ response = self.post(url)
514
516
  assert400(response)
515
517
 
516
518
  launch.assert_not_called()
517
519
 
518
520
  @pytest.mark.options(HARVEST_ENABLE_MANUAL_RUN=True)
519
- def test_cannot_run_source_if_not_owned(self, mocker: MockerFixture, api: ApiClient):
521
+ def test_cannot_run_source_if_not_owned(self, mocker: MockerFixture):
520
522
  launch = mocker.patch.object(actions.harvest, "delay")
521
523
  other_user = UserFactory()
522
- api.login()
524
+ self.login()
523
525
 
524
526
  source = HarvestSourceFactory(
525
527
  backend="factory",
@@ -528,15 +530,15 @@ class HarvestAPITest(MockBackendsMixin, PytestOnlyAPITestCase):
528
530
  )
529
531
 
530
532
  url = url_for("api.run_harvest_source", source=source)
531
- response = api.post(url)
533
+ response = self.post(url)
532
534
  assert403(response)
533
535
 
534
536
  launch.assert_not_called()
535
537
 
536
538
  @pytest.mark.options(HARVEST_ENABLE_MANUAL_RUN=True)
537
- def test_cannot_run_source_if_not_validated(self, mocker: MockerFixture, api: ApiClient):
539
+ def test_cannot_run_source_if_not_validated(self, mocker: MockerFixture):
538
540
  launch = mocker.patch.object(actions.harvest, "delay")
539
- user = api.login()
541
+ user = self.login()
540
542
 
541
543
  source = HarvestSourceFactory(
542
544
  backend="factory",
@@ -545,46 +547,46 @@ class HarvestAPITest(MockBackendsMixin, PytestOnlyAPITestCase):
545
547
  )
546
548
 
547
549
  url = url_for("api.run_harvest_source", source=source)
548
- response = api.post(url)
550
+ response = self.post(url)
549
551
  assert400(response)
550
552
 
551
553
  launch.assert_not_called()
552
554
 
553
- def test_source_from_config(self, api):
554
- api.login()
555
+ def test_source_from_config(self):
556
+ self.login()
555
557
  data = {"name": faker.word(), "url": faker.url(), "backend": "factory"}
556
- response = api.post(url_for("api.preview_harvest_source_config"), data)
558
+ response = self.post(url_for("api.preview_harvest_source_config"), data)
557
559
  assert200(response)
558
560
 
559
- def test_delete_source(self, api):
560
- user = api.login()
561
+ def test_delete_source(self):
562
+ user = self.login()
561
563
  source = HarvestSourceFactory(owner=user)
562
564
 
563
565
  url = url_for("api.harvest_source", source=source)
564
- response = api.delete(url)
566
+ response = self.delete(url)
565
567
  assert204(response)
566
568
 
567
569
  deleted_sources = HarvestSource.objects(deleted__exists=True)
568
570
  assert len(deleted_sources) == 1
569
571
 
570
- def test_delete_source_require_permission(self, api):
572
+ def test_delete_source_require_permission(self):
571
573
  """It should not delete a source if not the owner"""
572
- api.login()
574
+ self.login()
573
575
  source = HarvestSourceFactory()
574
576
 
575
577
  url = url_for("api.harvest_source", source=source)
576
- response = api.delete(url)
578
+ response = self.delete(url)
577
579
 
578
580
  assert403(response)
579
581
 
580
- def test_schedule_source(self, api):
582
+ def test_schedule_source(self):
581
583
  """It should allow to schedule a source if admin"""
582
- api.login(AdminFactory())
584
+ self.login(AdminFactory())
583
585
  source = HarvestSourceFactory()
584
586
 
585
587
  data = "0 0 * * *"
586
588
  url = url_for("api.schedule_harvest_source", source=source)
587
- response = api.post(url, data)
589
+ response = self.post(url, data)
588
590
  assert200(response)
589
591
 
590
592
  assert response.json["schedule"] == "0 0 * * *"
@@ -599,22 +601,22 @@ class HarvestAPITest(MockBackendsMixin, PytestOnlyAPITestCase):
599
601
  assert periodic_task.crontab.month_of_year == "*"
600
602
  assert periodic_task.enabled
601
603
 
602
- def test_schedule_source_is_admin_only(self, api):
604
+ def test_schedule_source_is_admin_only(self):
603
605
  """It should only allow admins to schedule a source"""
604
- api.login()
606
+ self.login()
605
607
  source = HarvestSourceFactory()
606
608
 
607
609
  data = "0 0 * * *"
608
610
  url = url_for("api.schedule_harvest_source", source=source)
609
- response = api.post(url, data)
611
+ response = self.post(url, data)
610
612
  assert403(response)
611
613
 
612
614
  source.reload()
613
615
  assert source.periodic_task is None
614
616
 
615
- def test_unschedule_source(self, api):
617
+ def test_unschedule_source(self):
616
618
  """It should allow to unschedule a source if admin"""
617
- api.login(AdminFactory())
619
+ self.login(AdminFactory())
618
620
  periodic_task = PeriodicTask.objects.create(
619
621
  task="harvest",
620
622
  name=faker.name(),
@@ -625,15 +627,15 @@ class HarvestAPITest(MockBackendsMixin, PytestOnlyAPITestCase):
625
627
  source = HarvestSourceFactory(periodic_task=periodic_task)
626
628
 
627
629
  url = url_for("api.schedule_harvest_source", source=source)
628
- response = api.delete(url)
630
+ response = self.delete(url)
629
631
  assert204(response)
630
632
 
631
633
  source.reload()
632
634
  assert source.periodic_task is None
633
635
 
634
- def test_unschedule_source_is_admin_only(self, api):
636
+ def test_unschedule_source_is_admin_only(self):
635
637
  """It should only allow admins to unschedule a source"""
636
- api.login()
638
+ self.login()
637
639
  periodic_task = PeriodicTask.objects.create(
638
640
  task="harvest",
639
641
  name=faker.name(),
@@ -644,8 +646,160 @@ class HarvestAPITest(MockBackendsMixin, PytestOnlyAPITestCase):
644
646
  source = HarvestSourceFactory(periodic_task=periodic_task)
645
647
 
646
648
  url = url_for("api.schedule_harvest_source", source=source)
647
- response = api.delete(url)
649
+ response = self.delete(url)
648
650
  assert403(response)
649
651
 
650
652
  source.reload()
651
653
  assert source.periodic_task is not None
654
+
655
+ def test_list_items(self):
656
+ """It should fetch the harvest items list from the API for a specific job"""
657
+ job = HarvestJobFactory(
658
+ items=[
659
+ HarvestItem(dataset=DatasetFactory()),
660
+ HarvestItem(dataservice=DataserviceFactory()),
661
+ HarvestItem(dataset=DatasetFactory(), remote_url="https://my.remote.example.com"),
662
+ ],
663
+ )
664
+ response = self.get(url_for("api.harvest_job", ident=str(job.id)))
665
+ assert200(response)
666
+ assert len(response.json["items"]) == 3
667
+ assert set(response.json["items"][0].keys()) == set(
668
+ [
669
+ "created",
670
+ "started",
671
+ "ended",
672
+ "dataset",
673
+ "dataservice",
674
+ "remote_url",
675
+ "remote_id",
676
+ "args",
677
+ "errors",
678
+ "kwargs",
679
+ "logs",
680
+ "status",
681
+ ]
682
+ )
683
+ # Make sure appropriate dataset or dataservice is set
684
+ assert response.json["items"][0]["dataset"] is not None
685
+ assert response.json["items"][0]["dataservice"] is None
686
+ assert response.json["items"][1]["dataset"] is None
687
+ assert response.json["items"][1]["dataservice"] is not None
688
+ # Make sure remote_url is exposed if exists
689
+ assert response.json["items"][1]["remote_url"] is None
690
+ assert response.json["items"][2]["remote_url"] == "https://my.remote.example.com"
691
+
692
+ def test_get_source_permissions_as_anonymous(self):
693
+ """It should return all permissions as False for anonymous users"""
694
+ source = HarvestSourceFactory()
695
+
696
+ url = url_for("api.harvest_source", source=source)
697
+ response = self.get(url)
698
+ assert200(response)
699
+
700
+ assert "permissions" in response.json
701
+ permissions = response.json["permissions"]
702
+ assert permissions["edit"] is False
703
+ assert permissions["delete"] is False
704
+ assert permissions["run"] is False
705
+ assert permissions["preview"] is False
706
+ assert permissions["validate"] is False
707
+ assert permissions["schedule"] is False
708
+
709
+ def test_get_source_permissions_as_owner(self):
710
+ """It should return owner permissions as True for source owner"""
711
+ user = self.login()
712
+ source = HarvestSourceFactory(owner=user)
713
+
714
+ url = url_for("api.harvest_source", source=source)
715
+ response = self.get(url)
716
+ assert200(response)
717
+
718
+ permissions = response.json["permissions"]
719
+ assert permissions["edit"] is True
720
+ assert permissions["delete"] is True
721
+ assert permissions["run"] is True
722
+ assert permissions["preview"] is True
723
+ assert permissions["validate"] is False
724
+ assert permissions["schedule"] is False
725
+
726
+ def test_get_source_permissions_as_org_admin(self):
727
+ """It should return owner permissions as True for org admins"""
728
+ user = self.login()
729
+ member = Member(user=user, role="admin")
730
+ org = OrganizationFactory(members=[member])
731
+ source = HarvestSourceFactory(organization=org)
732
+
733
+ url = url_for("api.harvest_source", source=source)
734
+ response = self.get(url)
735
+ assert200(response)
736
+
737
+ permissions = response.json["permissions"]
738
+ assert permissions["edit"] is True
739
+ assert permissions["delete"] is True
740
+ assert permissions["run"] is True
741
+ assert permissions["preview"] is True
742
+ assert permissions["validate"] is False
743
+ assert permissions["schedule"] is False
744
+
745
+ def test_get_source_permissions_as_org_editor(self):
746
+ """It should return only preview permission as True for org editors"""
747
+ user = self.login()
748
+ member = Member(user=user, role="editor")
749
+ org = OrganizationFactory(members=[member])
750
+ source = HarvestSourceFactory(organization=org)
751
+
752
+ url = url_for("api.harvest_source", source=source)
753
+ response = self.get(url)
754
+ assert200(response)
755
+
756
+ permissions = response.json["permissions"]
757
+ assert permissions["edit"] is False
758
+ assert permissions["delete"] is False
759
+ assert permissions["run"] is False
760
+ assert permissions["preview"] is True
761
+ assert permissions["validate"] is False
762
+ assert permissions["schedule"] is False
763
+
764
+ def test_get_source_permissions_as_superadmin(self):
765
+ """It should return all permissions as True for admin users"""
766
+ self.login(AdminFactory())
767
+ source = HarvestSourceFactory()
768
+
769
+ url = url_for("api.harvest_source", source=source)
770
+ response = self.get(url)
771
+ assert200(response)
772
+
773
+ permissions = response.json["permissions"]
774
+ assert permissions["edit"] is True
775
+ assert permissions["delete"] is True
776
+ assert permissions["run"] is True
777
+ assert permissions["preview"] is True
778
+ assert permissions["validate"] is True
779
+ assert permissions["schedule"] is True
780
+
781
+ def test_get_source_permissions_as_other_user(self):
782
+ """It should return all permissions as False for non-owner users"""
783
+ self.login()
784
+ source = HarvestSourceFactory() # owned by another user
785
+
786
+ url = url_for("api.harvest_source", source=source)
787
+ response = self.get(url)
788
+ assert200(response)
789
+
790
+ permissions = response.json["permissions"]
791
+ assert permissions["edit"] is False
792
+ assert permissions["delete"] is False
793
+ assert permissions["run"] is False
794
+ assert permissions["preview"] is False
795
+ assert permissions["validate"] is False
796
+ assert permissions["schedule"] is False
797
+
798
+ def test_preview_source_require_permission(self):
799
+ """It should not allow preview if not the owner"""
800
+ self.login()
801
+ source = HarvestSourceFactory() # owned by another user
802
+
803
+ url = url_for("api.preview_harvest_source", source=source)
804
+ response = self.get(url)
805
+ assert403(response)