udata 13.0.1.dev12__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 (177) hide show
  1. udata/api/__init__.py +2 -8
  2. udata/api_fields.py +35 -4
  3. udata/app.py +30 -50
  4. udata/auth/__init__.py +29 -6
  5. udata/auth/forms.py +8 -6
  6. udata/auth/views.py +6 -3
  7. udata/commands/__init__.py +2 -14
  8. udata/commands/db.py +13 -25
  9. udata/commands/info.py +0 -16
  10. udata/commands/serve.py +3 -11
  11. udata/commands/tests/test_fixtures.py +9 -9
  12. udata/core/access_type/api.py +1 -1
  13. udata/core/access_type/constants.py +12 -8
  14. udata/core/activity/api.py +5 -6
  15. udata/core/avatars/api.py +43 -0
  16. udata/core/avatars/test_avatar_api.py +30 -0
  17. udata/core/badges/tests/test_commands.py +6 -6
  18. udata/core/csv.py +5 -0
  19. udata/core/dataservices/models.py +15 -3
  20. udata/core/dataservices/tasks.py +7 -0
  21. udata/core/dataset/api.py +2 -0
  22. udata/core/dataset/models.py +2 -2
  23. udata/core/dataset/permissions.py +31 -0
  24. udata/core/dataset/tasks.py +50 -10
  25. udata/core/discussions/models.py +1 -0
  26. udata/core/metrics/__init__.py +0 -6
  27. udata/core/organization/api.py +8 -5
  28. udata/core/organization/mails.py +1 -1
  29. udata/core/organization/models.py +9 -1
  30. udata/core/organization/notifications.py +84 -0
  31. udata/core/organization/permissions.py +1 -1
  32. udata/core/organization/tasks.py +3 -0
  33. udata/core/pages/tests/test_api.py +32 -0
  34. udata/core/post/api.py +24 -69
  35. udata/core/post/models.py +84 -16
  36. udata/core/post/tests/test_api.py +24 -1
  37. udata/core/reports/api.py +18 -0
  38. udata/core/reports/models.py +42 -2
  39. udata/core/reuse/models.py +1 -1
  40. udata/core/reuse/tasks.py +7 -0
  41. udata/core/site/models.py +2 -6
  42. udata/core/spatial/commands.py +2 -4
  43. udata/core/spatial/forms.py +2 -2
  44. udata/core/spatial/models.py +0 -10
  45. udata/core/spatial/tests/test_api.py +1 -36
  46. udata/core/user/models.py +15 -2
  47. udata/cors.py +2 -5
  48. udata/db/migrations.py +279 -0
  49. udata/features/notifications/api.py +7 -18
  50. udata/features/notifications/models.py +56 -0
  51. udata/features/notifications/tasks.py +25 -0
  52. udata/flask_mongoengine/engine.py +0 -4
  53. udata/frontend/__init__.py +3 -122
  54. udata/frontend/markdown.py +2 -1
  55. udata/harvest/actions.py +24 -9
  56. udata/harvest/api.py +30 -22
  57. udata/harvest/backends/__init__.py +21 -9
  58. udata/harvest/backends/base.py +29 -3
  59. udata/harvest/backends/ckan/harvesters.py +13 -2
  60. udata/harvest/backends/dcat.py +3 -0
  61. udata/harvest/backends/maaf.py +1 -0
  62. udata/harvest/commands.py +39 -4
  63. udata/harvest/filters.py +17 -6
  64. udata/harvest/forms.py +9 -6
  65. udata/harvest/models.py +16 -0
  66. udata/harvest/permissions.py +27 -0
  67. udata/harvest/tasks.py +3 -5
  68. udata/harvest/tests/ckan/test_ckan_backend.py +35 -2
  69. udata/harvest/tests/ckan/test_ckan_backend_errors.py +1 -1
  70. udata/harvest/tests/ckan/test_ckan_backend_filters.py +1 -1
  71. udata/harvest/tests/ckan/test_dkan_backend.py +1 -1
  72. udata/harvest/tests/dcat/udata.xml +6 -6
  73. udata/harvest/tests/factories.py +1 -1
  74. udata/harvest/tests/test_actions.py +63 -8
  75. udata/harvest/tests/test_api.py +278 -123
  76. udata/harvest/tests/test_base_backend.py +88 -1
  77. udata/harvest/tests/test_dcat_backend.py +60 -13
  78. udata/harvest/tests/test_filters.py +6 -0
  79. udata/i18n.py +11 -273
  80. udata/mail.py +5 -1
  81. udata/migrations/2025-10-31-create-membership-request-notifications.py +55 -0
  82. udata/migrations/2025-11-13-delete-user-email-index.py +25 -0
  83. udata/migrations/2025-12-04-add-uuid-to-discussion-messages.py +28 -0
  84. udata/models/__init__.py +0 -8
  85. udata/mongo/slug_fields.py +1 -1
  86. udata/rdf.py +45 -6
  87. udata/routing.py +2 -10
  88. udata/sentry.py +4 -10
  89. udata/settings.py +23 -17
  90. udata/tasks.py +4 -3
  91. udata/templates/mail/message.html +5 -31
  92. udata/tests/__init__.py +28 -12
  93. udata/tests/api/__init__.py +108 -21
  94. udata/tests/api/test_activities_api.py +36 -0
  95. udata/tests/api/test_auth_api.py +121 -95
  96. udata/tests/api/test_base_api.py +7 -4
  97. udata/tests/api/test_dataservices_api.py +29 -1
  98. udata/tests/api/test_datasets_api.py +45 -21
  99. udata/tests/api/test_organizations_api.py +192 -197
  100. udata/tests/api/test_reports_api.py +157 -0
  101. udata/tests/api/test_reuses_api.py +147 -147
  102. udata/tests/api/test_security_api.py +12 -12
  103. udata/tests/api/test_swagger.py +4 -4
  104. udata/tests/api/test_tags_api.py +8 -8
  105. udata/tests/api/test_user_api.py +13 -1
  106. udata/tests/apiv2/test_swagger.py +4 -4
  107. udata/tests/apiv2/test_topics.py +1 -1
  108. udata/tests/cli/test_cli_base.py +8 -9
  109. udata/tests/dataset/test_dataset_commands.py +4 -4
  110. udata/tests/dataset/test_dataset_model.py +66 -26
  111. udata/tests/dataset/test_dataset_rdf.py +99 -5
  112. udata/tests/dataset/test_resource_preview.py +0 -1
  113. udata/tests/frontend/test_auth.py +24 -1
  114. udata/tests/frontend/test_csv.py +0 -3
  115. udata/tests/helpers.py +37 -27
  116. udata/tests/organization/test_notifications.py +67 -2
  117. udata/tests/plugin.py +6 -261
  118. udata/tests/site/test_site_csv_exports.py +22 -10
  119. udata/tests/test_activity.py +9 -9
  120. udata/tests/test_cors.py +1 -1
  121. udata/tests/test_dcat_commands.py +2 -2
  122. udata/tests/test_discussions.py +5 -5
  123. udata/tests/test_migrations.py +181 -481
  124. udata/tests/test_notifications.py +15 -57
  125. udata/tests/test_notifications_task.py +43 -0
  126. udata/tests/test_owned.py +81 -1
  127. udata/tests/test_storages.py +25 -19
  128. udata/tests/test_topics.py +77 -61
  129. udata/tests/test_uris.py +33 -0
  130. udata/tests/workers/test_jobs_commands.py +23 -23
  131. udata/translations/ar/LC_MESSAGES/udata.mo +0 -0
  132. udata/translations/ar/LC_MESSAGES/udata.po +187 -108
  133. udata/translations/de/LC_MESSAGES/udata.mo +0 -0
  134. udata/translations/de/LC_MESSAGES/udata.po +187 -108
  135. udata/translations/es/LC_MESSAGES/udata.mo +0 -0
  136. udata/translations/es/LC_MESSAGES/udata.po +187 -108
  137. udata/translations/fr/LC_MESSAGES/udata.mo +0 -0
  138. udata/translations/fr/LC_MESSAGES/udata.po +188 -109
  139. udata/translations/it/LC_MESSAGES/udata.mo +0 -0
  140. udata/translations/it/LC_MESSAGES/udata.po +187 -108
  141. udata/translations/pt/LC_MESSAGES/udata.mo +0 -0
  142. udata/translations/pt/LC_MESSAGES/udata.po +187 -108
  143. udata/translations/sr/LC_MESSAGES/udata.mo +0 -0
  144. udata/translations/sr/LC_MESSAGES/udata.po +187 -108
  145. udata/translations/udata.pot +215 -106
  146. udata/uris.py +0 -2
  147. udata/utils.py +5 -0
  148. udata-14.4.1.dev7.dist-info/METADATA +109 -0
  149. {udata-13.0.1.dev12.dist-info → udata-14.4.1.dev7.dist-info}/RECORD +153 -166
  150. {udata-13.0.1.dev12.dist-info → udata-14.4.1.dev7.dist-info}/entry_points.txt +3 -5
  151. udata/core/followers/views.py +0 -15
  152. udata/core/post/forms.py +0 -30
  153. udata/entrypoints.py +0 -93
  154. udata/features/identicon/__init__.py +0 -0
  155. udata/features/identicon/api.py +0 -13
  156. udata/features/identicon/backends.py +0 -131
  157. udata/features/identicon/tests/__init__.py +0 -0
  158. udata/features/identicon/tests/test_backends.py +0 -18
  159. udata/features/territories/__init__.py +0 -49
  160. udata/features/territories/api.py +0 -25
  161. udata/features/territories/models.py +0 -51
  162. udata/flask_mongoengine/json.py +0 -38
  163. udata/migrations/__init__.py +0 -367
  164. udata/templates/mail/base.html +0 -105
  165. udata/templates/mail/base.txt +0 -6
  166. udata/templates/mail/button.html +0 -3
  167. udata/templates/mail/layouts/1-column.html +0 -19
  168. udata/templates/mail/layouts/2-columns.html +0 -20
  169. udata/templates/mail/layouts/center-panel.html +0 -16
  170. udata/tests/cli/test_db_cli.py +0 -68
  171. udata/tests/features/territories/__init__.py +0 -20
  172. udata/tests/features/territories/test_territories_api.py +0 -185
  173. udata/tests/frontend/test_hooks.py +0 -149
  174. udata-13.0.1.dev12.dist-info/METADATA +0 -133
  175. {udata-13.0.1.dev12.dist-info → udata-14.4.1.dev7.dist-info}/WHEEL +0 -0
  176. {udata-13.0.1.dev12.dist-info → udata-14.4.1.dev7.dist-info}/licenses/LICENSE +0 -0
  177. {udata-13.0.1.dev12.dist-info → udata-14.4.1.dev7.dist-info}/top_level.txt +0 -0
udata/harvest/filters.py CHANGED
@@ -3,6 +3,9 @@ from voluptuous import Invalid
3
3
 
4
4
  from udata import tags, uris
5
5
 
6
+ TRUTHY_STRINGS = ("on", "t", "true", "y", "yes", "1")
7
+ FALSY_STRINGS = ("f", "false", "n", "no", "off", "0")
8
+
6
9
 
7
10
  def boolean(value):
8
11
  """
@@ -15,17 +18,25 @@ def boolean(value):
15
18
  if value is None or isinstance(value, bool):
16
19
  return value
17
20
 
18
- try:
19
- return bool(int(value))
20
- except ValueError:
21
+ if isinstance(value, int):
22
+ return bool(value)
23
+
24
+ if isinstance(value, str):
21
25
  lower_value = value.strip().lower()
26
+
22
27
  if not lower_value:
23
28
  return None
24
- if lower_value in ("f", "false", "n", "no", "off"):
29
+ if lower_value in FALSY_STRINGS:
25
30
  return False
26
- if lower_value in ("on", "t", "true", "y", "yes"):
31
+ if lower_value in TRUTHY_STRINGS:
27
32
  return True
28
- raise Invalid("Unable to parse boolean {0}".format(value))
33
+ raise Invalid(
34
+ f"Unable to parse string '{value}' as boolean. Supported values are {','.join(TRUTHY_STRINGS)} for `True` and {','.join(FALSY_STRINGS)} for `False`."
35
+ )
36
+
37
+ raise Invalid(
38
+ f"Cannot convert value {value} of type {type(value)} to boolean. Supported types are `bool`, `int` and `str`"
39
+ )
29
40
 
30
41
 
31
42
  def to_date(value):
udata/harvest/forms.py CHANGED
@@ -1,8 +1,8 @@
1
1
  from udata.forms import Form, fields, validators
2
+ from udata.harvest.backends import get_backend, get_enabled_backends
2
3
  from udata.i18n import lazy_gettext as _
3
4
  from udata.utils import safe_unicode
4
5
 
5
- from .actions import list_backends
6
6
  from .models import VALIDATION_REFUSED, VALIDATION_STATES
7
7
 
8
8
  __all__ = "HarvestSourceForm", "HarvestSourceValidationForm"
@@ -13,9 +13,6 @@ class HarvestConfigField(fields.DictField):
13
13
  A DictField with extras validations on known configurations
14
14
  """
15
15
 
16
- def get_backend(self, form):
17
- return next(b for b in list_backends() if b.name == form.backend.data)
18
-
19
16
  def get_filter_specs(self, backend, key):
20
17
  candidates = (f for f in backend.filters if f.key == key)
21
18
  return next(candidates, None)
@@ -30,7 +27,10 @@ class HarvestConfigField(fields.DictField):
30
27
 
31
28
  def pre_validate(self, form):
32
29
  if self.data:
33
- backend = self.get_backend(form)
30
+ backend = get_backend(form.backend.data)
31
+ if backend is None:
32
+ return # Should have been catch by the enum check for `form.backend`
33
+
34
34
  # Validate filters
35
35
  for f in self.data.get("filters") or []:
36
36
  if not ("key" in f and "value" in f):
@@ -49,6 +49,7 @@ class HarvestConfigField(fields.DictField):
49
49
  msg = '"{0}" filter should of type "{1}"'
50
50
  msg = msg.format(specs.key, specs.type.__name__)
51
51
  raise validators.ValidationError(msg)
52
+
52
53
  # Validate extras configs
53
54
  for f in self.data.get("extra_configs") or []:
54
55
  if not ("key" in f and "value" in f):
@@ -63,6 +64,7 @@ class HarvestConfigField(fields.DictField):
63
64
  msg = '"{0}" extra config should be of type "{1}"'
64
65
  msg = msg.format(specs.key, specs.type.__name__)
65
66
  raise validators.ValidationError(msg)
67
+
66
68
  # Validate features
67
69
  for key, value in (self.data.get("features") or {}).items():
68
70
  if not isinstance(value, bool):
@@ -81,7 +83,8 @@ class HarvestSourceForm(Form):
81
83
  )
82
84
  url = fields.URLField(_("URL"), [validators.DataRequired()])
83
85
  backend = fields.SelectField(
84
- _("Backend"), choices=lambda: [(b.name, b.display_name) for b in list_backends()]
86
+ _("Backend"),
87
+ choices=lambda: [(b.name, b.display_name) for b in get_enabled_backends().values()],
85
88
  )
86
89
  owner = fields.CurrentUserField()
87
90
  organization = fields.PublishAsField(_("Publish as"))
udata/harvest/models.py CHANGED
@@ -66,6 +66,7 @@ class HarvestLog(db.EmbeddedDocument):
66
66
 
67
67
  class HarvestItem(db.EmbeddedDocument):
68
68
  remote_id = db.StringField()
69
+ remote_url = db.StringField()
69
70
  dataset = db.ReferenceField(Dataset)
70
71
  dataservice = db.ReferenceField(Dataservice)
71
72
  status = db.StringField(
@@ -172,6 +173,21 @@ class HarvestSource(Owned, db.Document):
172
173
  def __str__(self):
173
174
  return self.name or ""
174
175
 
176
+ @property
177
+ def permissions(self):
178
+ from udata.auth import admin_permission
179
+
180
+ from .permissions import HarvestSourceAdminPermission, HarvestSourcePermission
181
+
182
+ return {
183
+ "edit": HarvestSourceAdminPermission(self),
184
+ "delete": HarvestSourceAdminPermission(self),
185
+ "run": HarvestSourceAdminPermission(self),
186
+ "preview": HarvestSourcePermission(self),
187
+ "validate": admin_permission,
188
+ "schedule": admin_permission,
189
+ }
190
+
175
191
 
176
192
  class HarvestJob(db.Document):
177
193
  """Keep track of harvestings"""
@@ -0,0 +1,27 @@
1
+ from udata.auth import Permission, UserNeed
2
+ from udata.core.dataset.permissions import OwnablePermission
3
+ from udata.core.organization.permissions import OrganizationAdminNeed
4
+
5
+
6
+ class HarvestSourcePermission(OwnablePermission):
7
+ """Permission for basic harvest source operations (preview)
8
+ Allows organization admins, editors, or owner.
9
+ """
10
+
11
+ pass
12
+
13
+
14
+ class HarvestSourceAdminPermission(Permission):
15
+ """Permission for sensitive harvest source operations (edit, delete, run)
16
+ Allows only organization admins or owner (not editors).
17
+ """
18
+
19
+ def __init__(self, source) -> None:
20
+ needs = []
21
+
22
+ if source.organization:
23
+ needs.append(OrganizationAdminNeed(source.organization.id))
24
+ elif source.owner:
25
+ needs.append(UserNeed(source.owner.fs_uniquifier))
26
+
27
+ super(HarvestSourceAdminPermission, self).__init__(*needs)
udata/harvest/tasks.py CHANGED
@@ -1,5 +1,3 @@
1
- from flask import current_app
2
-
3
1
  from udata.tasks import get_logger, job, task
4
2
 
5
3
  from . import backends
@@ -16,7 +14,7 @@ def harvest(self, ident):
16
14
  if source.deleted or not source.active:
17
15
  log.info('Ignoring inactive or deleted source "%s"', source.id)
18
16
  return # Ignore deleted and inactive sources
19
- Backend = backends.get(current_app, source.backend)
17
+ Backend = backends.get_backend(source.backend)
20
18
  backend = Backend(source)
21
19
 
22
20
  backend.harvest()
@@ -27,7 +25,7 @@ def harvest_job_item(job_id, item_id):
27
25
  log.info('Harvesting item %s for job "%s"', item_id, job_id)
28
26
 
29
27
  job = HarvestJob.objects.get(pk=job_id)
30
- Backend = backends.get(current_app, job.source.backend)
28
+ Backend = backends.get_backend(job.source.backend)
31
29
  backend = Backend(job)
32
30
 
33
31
  item = next(i for i in job.items if i.remote_id == item_id)
@@ -40,7 +38,7 @@ def harvest_job_item(job_id, item_id):
40
38
  def harvest_job_finalize(results, job_id):
41
39
  log.info('Finalize harvesting for job "%s"', job_id)
42
40
  job = HarvestJob.objects.get(pk=job_id)
43
- Backend = backends.get(current_app, job.source.backend)
41
+ Backend = backends.get_backend(job.source.backend)
44
42
  backend = Backend(job)
45
43
  backend.finalize()
46
44
 
@@ -200,6 +200,24 @@ def spatial_geom_multipolygon(resource_data):
200
200
  return data, {"multipolygon": multipolygon}
201
201
 
202
202
 
203
+ @pytest.fixture
204
+ def spatial_geom_polygon_as_dict(resource_data):
205
+ """
206
+ Test case where extra["value"] is already a dict in CKAN (e.g., datasud.fr).
207
+ In some CKAN instances, the spatial value is returned as a dict directly
208
+ instead of a JSON string, so json.loads() would fail.
209
+ """
210
+ polygon = faker.polygon()
211
+ data = {
212
+ "name": faker.unique_string(),
213
+ "title": faker.sentence(),
214
+ "notes": faker.paragraph(),
215
+ "resources": [resource_data],
216
+ "extras": [{"key": "spatial", "value": polygon}],
217
+ }
218
+ return data, {"polygon": polygon}
219
+
220
+
203
221
  @pytest.fixture
204
222
  def known_spatial_text_name(resource_data):
205
223
  zone = GeoZoneFactory()
@@ -364,7 +382,7 @@ def empty_extras(resource_data):
364
382
  ##############################################################################
365
383
 
366
384
 
367
- @pytest.mark.options(PLUGINS=["ckan"])
385
+ @pytest.mark.options(HARVESTER_BACKENDS=["ckan"])
368
386
  class CkanBackendTest(PytestOnlyDBTestCase):
369
387
  @pytest.mark.ckan_data("minimal")
370
388
  def test_minimal_metadata(self, data, result, kwargs):
@@ -422,6 +440,21 @@ class CkanBackendTest(PytestOnlyDBTestCase):
422
440
  dataset = dataset_for(result)
423
441
  assert dataset.spatial.geom == multipolygon
424
442
 
443
+ @pytest.mark.ckan_data("spatial_geom_polygon_as_dict")
444
+ def test_geospatial_geom_polygon_as_dict(self, result, kwargs):
445
+ """
446
+ Test that spatial geometry works when the value is already a dict.
447
+ Some CKAN instances (e.g., datasud.fr) return the spatial value as a dict
448
+ directly instead of a JSON string.
449
+ """
450
+ polygon = kwargs["polygon"]
451
+ dataset = dataset_for(result)
452
+
453
+ assert dataset.spatial.geom == {
454
+ "type": "MultiPolygon",
455
+ "coordinates": [polygon["coordinates"]],
456
+ }
457
+
425
458
  @pytest.mark.ckan_data("skipped_no_resources")
426
459
  def test_skip_no_resources(self, source, result):
427
460
  job = source.get_last_job()
@@ -505,7 +538,7 @@ class CkanBackendTest(PytestOnlyDBTestCase):
505
538
  ##############################################################################
506
539
 
507
540
 
508
- @pytest.mark.options(PLUGINS=["ckan"])
541
+ @pytest.mark.options(HARVESTER_BACKENDS=["ckan"])
509
542
  class CkanBackendEdgeCasesTest(PytestOnlyDBTestCase):
510
543
  def test_minimal_ckan_response(self, rmock):
511
544
  """CKAN Harvester should accept the minimum dataset payload"""
@@ -13,7 +13,7 @@ API_URL = "{}api/3/action/package_list".format(CKAN_URL)
13
13
  STATUS_CODE = (400, 500)
14
14
 
15
15
 
16
- @pytest.mark.options(PLUGINS=["ckan"])
16
+ @pytest.mark.options(HARVESTER_BACKENDS=["ckan"])
17
17
  class CkanBackendErrorsTest(PytestOnlyDBTestCase):
18
18
  @pytest.mark.parametrize("code", STATUS_CODE)
19
19
  def test_html_error(self, rmock, code):
@@ -8,7 +8,7 @@ from udata.tests.api import PytestOnlyDBTestCase
8
8
  from udata.utils import faker
9
9
 
10
10
 
11
- @pytest.mark.options(PLUGINS=["ckan"])
11
+ @pytest.mark.options(HARVESTER_BACKENDS=["ckan"])
12
12
  class CkanBackendFilterTest(PytestOnlyDBTestCase):
13
13
  def test_include_org_filter(self, ckan, rmock):
14
14
  source = HarvestSourceFactory(
@@ -16,7 +16,7 @@ def data_path(filename):
16
16
  return os.path.join(os.path.dirname(__file__), "data", filename)
17
17
 
18
18
 
19
- @pytest.mark.options(PLUGINS=["dkan"])
19
+ @pytest.mark.options(HARVESTER_BACKENDS=["dkan"])
20
20
  class DkanBackendTest(PytestOnlyDBTestCase):
21
21
  def test_dkan_french_w_license(self, rmock):
22
22
  """CKAN Harvester should accept the minimum dataset payload"""
@@ -37,7 +37,7 @@
37
37
  <dct:title>bureau-de-vote-vanves.csv</dct:title>
38
38
  <dct:description>Bureaux de vote - Vanves (csv)</dct:description>
39
39
  <dcat:downloadURL rdf:resource="https://vanves-seineouest.opendatasoft.com/api/explore/v2.1/catalog/datasets/bureau-de-vote-vanves/exports/csv?use_labels=true"/>
40
- <dcat:accessURL rdf:resource="https://www.data.gouv.fr/fr/datasets/r/5dd4e0b2-4d96-4f36-b73e-b78ec993703c"/>
40
+ <dcat:accessURL rdf:resource="https://www.data.gouv.fr/datasets/r/5dd4e0b2-4d96-4f36-b73e-b78ec993703c"/>
41
41
  <dct:issued rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2019-04-19T12:21:56</dct:issued>
42
42
  <dct:modified rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2019-04-19T12:21:56</dct:modified>
43
43
  <dct:rights>License Not Specified</dct:rights>
@@ -62,7 +62,7 @@
62
62
  <dct:title>bureau-de-vote-vanves.geojson</dct:title>
63
63
  <dct:description>Bureaux de vote - Vanves (geojson)</dct:description>
64
64
  <dcat:downloadURL rdf:resource="https://vanves-seineouest.opendatasoft.com/api/explore/v2.1/catalog/datasets/bureau-de-vote-vanves/exports/geojson"/>
65
- <dcat:accessURL rdf:resource="https://www.data.gouv.fr/fr/datasets/r/d78d1245-6e8e-44d8-88cd-87b1dd66034f"/>
65
+ <dcat:accessURL rdf:resource="https://www.data.gouv.fr/datasets/r/d78d1245-6e8e-44d8-88cd-87b1dd66034f"/>
66
66
  <dct:issued rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2019-04-19T12:21:56</dct:issued>
67
67
  <dct:modified rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2019-04-19T12:21:56</dct:modified>
68
68
  <dct:rights>License Not Specified</dct:rights>
@@ -86,7 +86,7 @@
86
86
  <dct:title>vfe_public_219200755_20240506.json</dct:title>
87
87
  <dct:description>Ville de Vanves - Part des véhicules à faibles émissions dans le renouvellement du parc (json)</dct:description>
88
88
  <dcat:downloadURL rdf:resource="https://vanves-seineouest.opendatasoft.com/api/explore/v2.1/catalog/datasets/vfe_public_219200755_20240506/exports/json"/>
89
- <dcat:accessURL rdf:resource="https://www.data.gouv.fr/fr/datasets/r/482677b8-379c-45a5-83ef-0361fecb4cc3"/>
89
+ <dcat:accessURL rdf:resource="https://www.data.gouv.fr/datasets/r/482677b8-379c-45a5-83ef-0361fecb4cc3"/>
90
90
  <dct:issued rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2024-05-06T15:15:18</dct:issued>
91
91
  <dct:modified rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2024-05-06T15:15:18</dct:modified>
92
92
  <dct:rights>License Not Specified</dct:rights>
@@ -99,7 +99,7 @@
99
99
  <dct:title>bureau-de-vote-vanves.zip</dct:title>
100
100
  <dct:description>Bureaux de vote - Vanves (shp)</dct:description>
101
101
  <dcat:downloadURL rdf:resource="https://vanves-seineouest.opendatasoft.com/api/explore/v2.1/catalog/datasets/bureau-de-vote-vanves/exports/shp"/>
102
- <dcat:accessURL rdf:resource="https://www.data.gouv.fr/fr/datasets/r/b18fb2bd-6c8b-47a8-84fb-cdc8e29f4f1a"/>
102
+ <dcat:accessURL rdf:resource="https://www.data.gouv.fr/datasets/r/b18fb2bd-6c8b-47a8-84fb-cdc8e29f4f1a"/>
103
103
  <dct:issued rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2019-04-19T12:21:56</dct:issued>
104
104
  <dct:modified rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2019-04-19T12:21:56</dct:modified>
105
105
  <dct:rights>License Not Specified</dct:rights>
@@ -112,7 +112,7 @@
112
112
  <dct:title>vfe_public_219200755_20240506.csv</dct:title>
113
113
  <dct:description>Ville de Vanves - Part des véhicules à faibles émissions dans le renouvellement du parc (csv)</dct:description>
114
114
  <dcat:downloadURL rdf:resource="https://vanves-seineouest.opendatasoft.com/api/explore/v2.1/catalog/datasets/vfe_public_219200755_20240506/exports/csv?use_labels=true"/>
115
- <dcat:accessURL rdf:resource="https://www.data.gouv.fr/fr/datasets/r/aab4d337-a617-4c18-a045-291d68787a7d"/>
115
+ <dcat:accessURL rdf:resource="https://www.data.gouv.fr/datasets/r/aab4d337-a617-4c18-a045-291d68787a7d"/>
116
116
  <dct:issued rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2024-05-06T15:15:18</dct:issued>
117
117
  <dct:modified rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2024-05-06T15:15:18</dct:modified>
118
118
  <dct:rights>License Not Specified</dct:rights>
@@ -161,7 +161,7 @@
161
161
  <dct:title>bureau-de-vote-vanves.json</dct:title>
162
162
  <dct:description>Bureaux de vote - Vanves (json)</dct:description>
163
163
  <dcat:downloadURL rdf:resource="https://vanves-seineouest.opendatasoft.com/api/explore/v2.1/catalog/datasets/bureau-de-vote-vanves/exports/json"/>
164
- <dcat:accessURL rdf:resource="https://www.data.gouv.fr/fr/datasets/r/0f80d285-72f8-49f8-a691-1dad6bd2f6db"/>
164
+ <dcat:accessURL rdf:resource="https://www.data.gouv.fr/datasets/r/0f80d285-72f8-49f8-a691-1dad6bd2f6db"/>
165
165
  <dct:issued rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2019-04-19T12:21:56</dct:issued>
166
166
  <dct:modified rdf:datatype="http://www.w3.org/2001/XMLSchema#dateTime">2019-04-19T12:21:56</dct:modified>
167
167
  <dct:rights>License Not Specified</dct:rights>
@@ -79,4 +79,4 @@ class MockBackendsMixin(object):
79
79
  @pytest.fixture(autouse=True)
80
80
  def mock_backend(self, mocker):
81
81
  return_value = {"factory": FactoryBackend}
82
- mocker.patch("udata.harvest.backends.get_all", return_value=return_value)
82
+ mocker.patch("udata.harvest.backends.get_all_backends", return_value=return_value)
@@ -11,17 +11,18 @@ from udata.core.activity.models import new_activity
11
11
  from udata.core.dataservices.factories import DataserviceFactory
12
12
  from udata.core.dataservices.models import HarvestMetadata as HarvestDataserviceMetadata
13
13
  from udata.core.dataset.activities import UserCreatedDataset
14
- from udata.core.dataset.factories import DatasetFactory
15
- from udata.core.dataset.models import HarvestDatasetMetadata
14
+ from udata.core.dataset.factories import DatasetFactory, ResourceFactory
15
+ from udata.core.dataset.models import HarvestDatasetMetadata, HarvestResourceMetadata
16
16
  from udata.core.organization.factories import OrganizationFactory
17
17
  from udata.core.user.factories import UserFactory
18
+ from udata.harvest.backends import get_enabled_backends
19
+ from udata.harvest.backends.base import BaseBackend
18
20
  from udata.models import Dataset, PeriodicTask
19
21
  from udata.tests.api import PytestOnlyDBTestCase
20
22
  from udata.tests.helpers import assert_emit, assert_equal_dates, assert_not_emit
21
23
  from udata.utils import faker
22
24
 
23
25
  from .. import actions, signals
24
- from ..backends import BaseBackend
25
26
  from ..models import (
26
27
  VALIDATION_ACCEPTED,
27
28
  VALIDATION_PENDING,
@@ -42,9 +43,10 @@ from .factories import (
42
43
  log = logging.getLogger(__name__)
43
44
 
44
45
 
45
- class HarvestActionsTest(PytestOnlyDBTestCase):
46
+ class HarvestActionsTest(MockBackendsMixin, PytestOnlyDBTestCase):
46
47
  def test_list_backends(self):
47
- for backend in actions.list_backends():
48
+ assert len(get_enabled_backends()) > 0
49
+ for backend in get_enabled_backends().values():
48
50
  assert issubclass(backend, BaseBackend)
49
51
 
50
52
  def test_list_sources(self):
@@ -269,7 +271,16 @@ class HarvestActionsTest(PytestOnlyDBTestCase):
269
271
  assert periodic_task.crontab.day_of_month == "*"
270
272
  assert periodic_task.crontab.month_of_year == "*"
271
273
  assert periodic_task.enabled
272
- assert periodic_task.name == "Harvest {0}".format(source.name)
274
+ assert periodic_task.name == f"Harvest {source.name} ({source.id})"
275
+
276
+ def test_double_schedule_with_same_name(self):
277
+ source_1 = HarvestSourceFactory(name="A")
278
+ source_2 = HarvestSourceFactory(name="A")
279
+
280
+ actions.schedule(source_1, hour=0)
281
+ actions.schedule(source_2, hour=0)
282
+
283
+ assert len(PeriodicTask.objects) == 2
273
284
 
274
285
  def test_schedule_from_cron(self):
275
286
  source = HarvestSourceFactory()
@@ -286,7 +297,7 @@ class HarvestActionsTest(PytestOnlyDBTestCase):
286
297
  assert periodic_task.crontab.month_of_year == "3"
287
298
  assert periodic_task.crontab.day_of_week == "sunday"
288
299
  assert periodic_task.enabled
289
- assert periodic_task.name == "Harvest {0}".format(source.name)
300
+ assert periodic_task.name == f"Harvest {source.name} ({source.id})"
290
301
 
291
302
  def test_reschedule(self):
292
303
  source = HarvestSourceFactory()
@@ -306,7 +317,7 @@ class HarvestActionsTest(PytestOnlyDBTestCase):
306
317
  assert periodic_task.crontab.day_of_month == "*"
307
318
  assert periodic_task.crontab.month_of_year == "*"
308
319
  assert periodic_task.enabled
309
- assert periodic_task.name == "Harvest {0}".format(source.name)
320
+ assert periodic_task.name == f"Harvest {source.name} ({source.id})"
310
321
 
311
322
  def test_unschedule(self):
312
323
  periodic_task = PeriodicTask.objects.create(
@@ -449,6 +460,50 @@ class HarvestActionsTest(PytestOnlyDBTestCase):
449
460
  assert result.success == len(datasets)
450
461
  assert result.errors == 1
451
462
 
463
+ def test_detach(self):
464
+ dataset = DatasetFactory(
465
+ harvest=HarvestDatasetMetadata(
466
+ source_id="source id", domain="test.org", remote_id="id"
467
+ ),
468
+ resources=[
469
+ ResourceFactory(
470
+ harvest=HarvestResourceMetadata(issued_at=datetime.now(), uri="test.org")
471
+ )
472
+ ],
473
+ )
474
+
475
+ actions.detach(dataset)
476
+
477
+ dataset.reload()
478
+ assert dataset.harvest is None
479
+ for resource in dataset.resources:
480
+ assert resource.harvest is None
481
+
482
+ def test_detach_all(self):
483
+ source = HarvestSourceFactory()
484
+ datasets = [
485
+ DatasetFactory(
486
+ harvest=HarvestDatasetMetadata(
487
+ source_id=str(source.id), domain="test.org", remote_id=str(i)
488
+ ),
489
+ resources=[
490
+ ResourceFactory(
491
+ harvest=HarvestResourceMetadata(issued_at=datetime.now(), uri="test.org")
492
+ )
493
+ ],
494
+ )
495
+ for i in range(3)
496
+ ]
497
+
498
+ result = actions.detach_all_from_source(source)
499
+
500
+ assert result == len(datasets)
501
+ for dataset in datasets:
502
+ dataset.reload()
503
+ assert dataset.harvest is None
504
+ for resource in dataset.resources:
505
+ assert resource.harvest is None
506
+
452
507
 
453
508
  class ExecutionTestMixin(MockBackendsMixin, PytestOnlyDBTestCase):
454
509
  def action(self, *args, **kwargs):