udata 12.0.2.dev10__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 (272) 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 -4
  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 +10 -12
  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.py +15 -24
  29. udata/core/dataset/api_fields.py +11 -0
  30. udata/core/dataset/apiv2.py +11 -0
  31. udata/core/dataset/constants.py +0 -1
  32. udata/core/dataset/forms.py +29 -0
  33. udata/core/dataset/models.py +24 -42
  34. udata/core/dataset/rdf.py +2 -1
  35. udata/core/dataset/search.py +2 -2
  36. udata/core/dataset/tasks.py +86 -8
  37. udata/core/discussions/mails.py +63 -0
  38. udata/core/discussions/tasks.py +4 -18
  39. udata/core/metrics/__init__.py +0 -6
  40. udata/core/organization/api.py +20 -14
  41. udata/core/organization/mails.py +144 -0
  42. udata/core/organization/models.py +2 -1
  43. udata/core/organization/rdf.py +3 -3
  44. udata/core/organization/search.py +1 -1
  45. udata/core/organization/tasks.py +21 -49
  46. udata/core/pages/tests/test_api.py +0 -2
  47. udata/core/reuse/api.py +29 -3
  48. udata/core/reuse/mails.py +21 -0
  49. udata/core/reuse/models.py +10 -1
  50. udata/core/reuse/search.py +1 -1
  51. udata/core/reuse/tasks.py +2 -3
  52. udata/core/site/api.py +27 -19
  53. udata/core/site/models.py +2 -6
  54. udata/core/site/rdf.py +2 -2
  55. udata/core/spatial/tests/test_api.py +17 -20
  56. udata/core/spatial/tests/test_models.py +3 -3
  57. udata/core/user/mails.py +54 -0
  58. udata/core/user/models.py +2 -3
  59. udata/core/user/tasks.py +8 -23
  60. udata/core/user/tests/test_user_model.py +2 -6
  61. udata/entrypoints.py +0 -6
  62. udata/features/identicon/tests/test_backends.py +3 -13
  63. udata/forms/fields.py +3 -3
  64. udata/forms/widgets.py +2 -2
  65. udata/frontend/__init__.py +3 -32
  66. udata/harvest/actions.py +4 -9
  67. udata/harvest/api.py +5 -14
  68. udata/harvest/backends/__init__.py +20 -11
  69. udata/harvest/backends/base.py +2 -2
  70. udata/harvest/backends/ckan/harvesters.py +2 -1
  71. udata/harvest/backends/dcat.py +3 -0
  72. udata/harvest/backends/maaf.py +1 -0
  73. udata/harvest/commands.py +6 -4
  74. udata/harvest/forms.py +9 -6
  75. udata/harvest/tasks.py +3 -5
  76. udata/harvest/tests/ckan/test_ckan_backend.py +300 -337
  77. udata/harvest/tests/ckan/test_ckan_backend_errors.py +94 -99
  78. udata/harvest/tests/ckan/test_ckan_backend_filters.py +128 -122
  79. udata/harvest/tests/ckan/test_dkan_backend.py +39 -51
  80. udata/harvest/tests/dcat/bnodes.xml +17 -1
  81. udata/harvest/tests/dcat/datara--5a26b0f6-0ccf-46ad-ac58-734054b91977.rdf.xml +255 -0
  82. udata/harvest/tests/dcat/datara--f40c3860-7236-4b30-a141-23b8ae33f7b2.rdf.xml +289 -0
  83. udata/harvest/tests/factories.py +1 -1
  84. udata/harvest/tests/test_actions.py +11 -9
  85. udata/harvest/tests/test_api.py +4 -5
  86. udata/harvest/tests/test_base_backend.py +5 -4
  87. udata/harvest/tests/test_dcat_backend.py +72 -16
  88. udata/harvest/tests/test_models.py +2 -4
  89. udata/harvest/tests/test_notifications.py +2 -4
  90. udata/harvest/tests/test_tasks.py +2 -3
  91. udata/mail.py +90 -53
  92. udata/migrations/2025-01-05-dataservices-fields-changes.py +8 -14
  93. udata/migrations/2025-10-21-remove-ckan-harvest-modified-at.py +28 -0
  94. udata/migrations/2025-10-29-harvesters-sources-integrity.py +27 -0
  95. udata/models/__init__.py +0 -2
  96. udata/mongo/extras_fields.py +4 -3
  97. udata/mongo/taglist_field.py +3 -3
  98. udata/rdf.py +65 -20
  99. udata/sentry.py +3 -4
  100. udata/settings.py +15 -13
  101. udata/tags.py +5 -5
  102. udata/tasks.py +3 -3
  103. udata/templates/mail/message.html +65 -0
  104. udata/templates/mail/message.txt +16 -0
  105. udata/tests/__init__.py +40 -58
  106. udata/tests/api/__init__.py +87 -2
  107. udata/tests/api/test_activities_api.py +17 -23
  108. udata/tests/api/test_auth_api.py +2 -4
  109. udata/tests/api/test_contact_points.py +48 -54
  110. udata/tests/api/test_dataservices_api.py +65 -97
  111. udata/tests/api/test_datasets_api.py +171 -56
  112. udata/tests/api/test_me_api.py +4 -6
  113. udata/tests/api/test_organizations_api.py +19 -38
  114. udata/tests/api/test_reports_api.py +0 -4
  115. udata/tests/api/test_reuses_api.py +99 -23
  116. udata/tests/api/test_security_api.py +124 -0
  117. udata/tests/api/test_swagger.py +2 -3
  118. udata/tests/api/test_tags_api.py +6 -7
  119. udata/tests/api/test_transfer_api.py +0 -2
  120. udata/tests/api/test_user_api.py +8 -10
  121. udata/tests/apiv2/test_datasets.py +0 -4
  122. udata/tests/apiv2/test_me_api.py +0 -2
  123. udata/tests/apiv2/test_organizations.py +0 -2
  124. udata/tests/apiv2/test_swagger.py +2 -3
  125. udata/tests/apiv2/test_topics.py +0 -2
  126. udata/tests/cli/test_cli_base.py +14 -12
  127. udata/tests/cli/test_db_cli.py +51 -54
  128. udata/tests/contact_point/test_contact_point_models.py +2 -2
  129. udata/tests/dataservice/test_csv_adapter.py +2 -5
  130. udata/tests/dataservice/test_dataservice_rdf.py +64 -4
  131. udata/tests/dataservice/test_dataservice_tasks.py +36 -38
  132. udata/tests/dataset/test_csv_adapter.py +2 -5
  133. udata/tests/dataset/test_dataset_actions.py +2 -4
  134. udata/tests/dataset/test_dataset_commands.py +2 -4
  135. udata/tests/dataset/test_dataset_events.py +3 -3
  136. udata/tests/dataset/test_dataset_model.py +6 -7
  137. udata/tests/dataset/test_dataset_rdf.py +205 -16
  138. udata/tests/dataset/test_dataset_recommendations.py +2 -2
  139. udata/tests/dataset/test_dataset_tasks.py +66 -68
  140. udata/tests/dataset/test_resource_preview.py +39 -48
  141. udata/tests/dataset/test_transport_tasks.py +2 -2
  142. udata/tests/features/territories/__init__.py +0 -6
  143. udata/tests/features/territories/test_territories_api.py +25 -24
  144. udata/tests/forms/test_current_user_field.py +2 -2
  145. udata/tests/forms/test_dict_field.py +2 -4
  146. udata/tests/forms/test_extras_fields.py +2 -3
  147. udata/tests/forms/test_image_field.py +2 -2
  148. udata/tests/forms/test_model_field.py +2 -4
  149. udata/tests/forms/test_publish_as_field.py +2 -4
  150. udata/tests/forms/test_user_forms.py +26 -29
  151. udata/tests/frontend/test_auth.py +2 -3
  152. udata/tests/frontend/test_csv.py +5 -6
  153. udata/tests/frontend/test_error_handlers.py +2 -3
  154. udata/tests/frontend/test_hooks.py +5 -7
  155. udata/tests/frontend/test_markdown.py +3 -4
  156. udata/tests/helpers.py +2 -7
  157. udata/tests/metrics/test_metrics.py +52 -48
  158. udata/tests/metrics/test_tasks.py +154 -150
  159. udata/tests/organization/test_csv_adapter.py +2 -5
  160. udata/tests/organization/test_notifications.py +2 -4
  161. udata/tests/organization/test_organization_model.py +3 -4
  162. udata/tests/organization/test_organization_rdf.py +6 -12
  163. udata/tests/plugin.py +6 -110
  164. udata/tests/reuse/test_reuse_model.py +3 -4
  165. udata/tests/site/test_site_api.py +0 -2
  166. udata/tests/site/test_site_csv_exports.py +0 -2
  167. udata/tests/site/test_site_metrics.py +2 -4
  168. udata/tests/site/test_site_model.py +2 -2
  169. udata/tests/site/test_site_rdf.py +85 -29
  170. udata/tests/test_activity.py +3 -3
  171. udata/tests/test_api_fields.py +6 -9
  172. udata/tests/test_cors.py +0 -2
  173. udata/tests/test_dcat_commands.py +2 -3
  174. udata/tests/test_discussions.py +2 -7
  175. udata/tests/test_mail.py +150 -114
  176. udata/tests/test_migrations.py +413 -419
  177. udata/tests/test_model.py +10 -11
  178. udata/tests/test_notifications.py +2 -3
  179. udata/tests/test_owned.py +3 -3
  180. udata/tests/test_rdf.py +19 -15
  181. udata/tests/test_routing.py +5 -5
  182. udata/tests/test_storages.py +6 -5
  183. udata/tests/test_tags.py +2 -4
  184. udata/tests/test_topics.py +2 -4
  185. udata/tests/test_transfer.py +4 -5
  186. udata/tests/topic/test_topic_tasks.py +25 -27
  187. udata/tests/user/test_user_rdf.py +2 -8
  188. udata/tests/user/test_user_tasks.py +3 -5
  189. udata/tests/workers/test_jobs_commands.py +2 -2
  190. udata/tests/workers/test_tasks_routing.py +27 -27
  191. udata/translations/ar/LC_MESSAGES/udata.mo +0 -0
  192. udata/translations/ar/LC_MESSAGES/udata.po +369 -435
  193. udata/translations/de/LC_MESSAGES/udata.mo +0 -0
  194. udata/translations/de/LC_MESSAGES/udata.po +371 -437
  195. udata/translations/es/LC_MESSAGES/udata.mo +0 -0
  196. udata/translations/es/LC_MESSAGES/udata.po +369 -435
  197. udata/translations/fr/LC_MESSAGES/udata.mo +0 -0
  198. udata/translations/fr/LC_MESSAGES/udata.po +381 -447
  199. udata/translations/it/LC_MESSAGES/udata.mo +0 -0
  200. udata/translations/it/LC_MESSAGES/udata.po +371 -437
  201. udata/translations/pt/LC_MESSAGES/udata.mo +0 -0
  202. udata/translations/pt/LC_MESSAGES/udata.po +371 -437
  203. udata/translations/sr/LC_MESSAGES/udata.mo +0 -0
  204. udata/translations/sr/LC_MESSAGES/udata.po +372 -438
  205. udata/translations/udata.pot +379 -440
  206. udata/utils.py +66 -4
  207. {udata-12.0.2.dev10.dist-info → udata-13.0.1.dev21.dist-info}/METADATA +1 -4
  208. {udata-12.0.2.dev10.dist-info → udata-13.0.1.dev21.dist-info}/RECORD +212 -256
  209. udata/linkchecker/__init__.py +0 -0
  210. udata/linkchecker/backends.py +0 -31
  211. udata/linkchecker/checker.py +0 -75
  212. udata/linkchecker/commands.py +0 -21
  213. udata/linkchecker/models.py +0 -9
  214. udata/linkchecker/tasks.py +0 -55
  215. udata/templates/mail/account_deleted.html +0 -5
  216. udata/templates/mail/account_deleted.txt +0 -6
  217. udata/templates/mail/account_inactivity.html +0 -40
  218. udata/templates/mail/account_inactivity.txt +0 -31
  219. udata/templates/mail/badge_added_association.html +0 -33
  220. udata/templates/mail/badge_added_association.txt +0 -11
  221. udata/templates/mail/badge_added_certified.html +0 -33
  222. udata/templates/mail/badge_added_certified.txt +0 -11
  223. udata/templates/mail/badge_added_company.html +0 -33
  224. udata/templates/mail/badge_added_company.txt +0 -11
  225. udata/templates/mail/badge_added_local_authority.html +0 -33
  226. udata/templates/mail/badge_added_local_authority.txt +0 -11
  227. udata/templates/mail/badge_added_public_service.html +0 -33
  228. udata/templates/mail/badge_added_public_service.txt +0 -11
  229. udata/templates/mail/discussion_closed.html +0 -47
  230. udata/templates/mail/discussion_closed.txt +0 -16
  231. udata/templates/mail/inactive_account_deleted.html +0 -5
  232. udata/templates/mail/inactive_account_deleted.txt +0 -6
  233. udata/templates/mail/membership_refused.html +0 -20
  234. udata/templates/mail/membership_refused.txt +0 -11
  235. udata/templates/mail/membership_request.html +0 -46
  236. udata/templates/mail/membership_request.txt +0 -12
  237. udata/templates/mail/new_discussion.html +0 -44
  238. udata/templates/mail/new_discussion.txt +0 -15
  239. udata/templates/mail/new_discussion_comment.html +0 -45
  240. udata/templates/mail/new_discussion_comment.txt +0 -16
  241. udata/templates/mail/new_member.html +0 -27
  242. udata/templates/mail/new_member.txt +0 -11
  243. udata/templates/mail/new_reuse.html +0 -37
  244. udata/templates/mail/new_reuse.txt +0 -9
  245. udata/templates/mail/test.html +0 -6
  246. udata/templates/mail/test.txt +0 -6
  247. udata/templates/mail/user_mail_card.html +0 -26
  248. udata/templates/security/email/base.html +0 -105
  249. udata/templates/security/email/base.txt +0 -6
  250. udata/templates/security/email/button.html +0 -3
  251. udata/templates/security/email/change_notice.html +0 -22
  252. udata/templates/security/email/change_notice.txt +0 -8
  253. udata/templates/security/email/confirmation_instructions.html +0 -20
  254. udata/templates/security/email/confirmation_instructions.txt +0 -7
  255. udata/templates/security/email/login_instructions.html +0 -19
  256. udata/templates/security/email/login_instructions.txt +0 -7
  257. udata/templates/security/email/reset_instructions.html +0 -24
  258. udata/templates/security/email/reset_instructions.txt +0 -9
  259. udata/templates/security/email/reset_notice.html +0 -11
  260. udata/templates/security/email/reset_notice.txt +0 -4
  261. udata/templates/security/email/welcome.html +0 -24
  262. udata/templates/security/email/welcome.txt +0 -9
  263. udata/templates/security/email/welcome_existing.html +0 -32
  264. udata/templates/security/email/welcome_existing.txt +0 -14
  265. udata/terms.md +0 -6
  266. udata/tests/frontend/__init__.py +0 -23
  267. udata/tests/metrics/conftest.py +0 -15
  268. udata/tests/test_linkchecker.py +0 -277
  269. {udata-12.0.2.dev10.dist-info → udata-13.0.1.dev21.dist-info}/WHEEL +0 -0
  270. {udata-12.0.2.dev10.dist-info → udata-13.0.1.dev21.dist-info}/entry_points.txt +0 -0
  271. {udata-12.0.2.dev10.dist-info → udata-13.0.1.dev21.dist-info}/licenses/LICENSE +0 -0
  272. {udata-12.0.2.dev10.dist-info → udata-13.0.1.dev21.dist-info}/top_level.txt +0 -0
@@ -1,9 +1,15 @@
1
- from udata.tasks import get_logger, task
1
+ from typing import Any
2
+
3
+ from mongoengine.base import TopLevelDocumentMetaclass
4
+
5
+ from udata.tasks import get_logger, job, task
2
6
 
3
7
  from .signals import on_badge_added
4
8
 
5
9
  log = get_logger(__name__)
6
10
 
11
+ _badge_jobs: dict[tuple[TopLevelDocumentMetaclass, str], Any] = {}
12
+
7
13
 
8
14
  def notify_new_badge(cls, kind):
9
15
  def wrapper(func):
@@ -17,3 +23,31 @@ def notify_new_badge(cls, kind):
17
23
  return t
18
24
 
19
25
  return wrapper
26
+
27
+
28
+ def register(model: TopLevelDocumentMetaclass, badge: str):
29
+ """Register a job to update some badge"""
30
+
31
+ def inner(func):
32
+ _badge_jobs[(model, badge)] = func
33
+ return func
34
+
35
+ return inner
36
+
37
+
38
+ def get_badge_job(model, badge):
39
+ return _badge_jobs.get((model, badge))
40
+
41
+
42
+ @job(name="update-badges")
43
+ def update_badges(self, badges: list[str] = []) -> None:
44
+ from udata.core.dataservices.models import Dataservice
45
+ from udata.models import Dataset, Organization, Reuse
46
+
47
+ for model in [Dataset, Reuse, Organization, Dataservice]:
48
+ for badge in model.__badges__:
49
+ if badges and badge not in badges:
50
+ continue
51
+ if adapter := get_badge_job(model, badge):
52
+ log.info(f"Running {model.__name__} {badge} job")
53
+ adapter()
@@ -1,13 +1,11 @@
1
1
  from tempfile import NamedTemporaryFile
2
2
 
3
- import pytest
4
-
5
3
  from udata.core.organization.constants import CERTIFIED, PUBLIC_SERVICE
6
4
  from udata.core.organization.factories import OrganizationFactory
5
+ from udata.tests.api import PytestOnlyDBTestCase
7
6
 
8
7
 
9
- @pytest.mark.usefixtures("clean_db")
10
- class BadgeCommandTest:
8
+ class BadgeCommandTest(PytestOnlyDBTestCase):
11
9
  def toggle(self, path_or_id, kind):
12
10
  return self.cli("badges", "toggle", path_or_id, kind)
13
11
 
@@ -2,7 +2,7 @@ from udata.api_fields import field
2
2
  from udata.auth import login_user
3
3
  from udata.core.user.factories import UserFactory
4
4
  from udata.mongo import db
5
- from udata.tests import DBTestMixin, TestCase
5
+ from udata.tests.api import DBTestCase
6
6
 
7
7
  from ..models import Badge, BadgeMixin, BadgesList
8
8
 
@@ -33,7 +33,7 @@ class Fake(db.Document, FakeBadgeMixin):
33
33
  pass
34
34
 
35
35
 
36
- class BadgeMixinTest(DBTestMixin, TestCase):
36
+ class BadgeMixinTest(DBTestCase):
37
37
  def test_attributes(self):
38
38
  """It should have a badge list"""
39
39
  fake = Fake.objects.create()
@@ -0,0 +1,55 @@
1
+ import udata.core.dataservices.tasks # noqa
2
+ import udata.core.dataset.tasks # noqa
3
+ from udata.core.badges.tasks import update_badges
4
+ from udata.core.constants import HVD
5
+ from udata.core.dataservices.factories import DataserviceFactory
6
+ from udata.core.dataservices.models import Dataservice
7
+ from udata.core.dataset.factories import DatasetFactory
8
+ from udata.core.dataset.models import Dataset
9
+ from udata.core.organization.constants import CERTIFIED, PUBLIC_SERVICE
10
+ from udata.core.organization.factories import OrganizationFactory
11
+ from udata.tests.api import DBTestCase
12
+
13
+
14
+ class BadgeTasksTest(DBTestCase):
15
+ def test_update_badges(self):
16
+ """
17
+ Test update_badges run the appropriate badge update jobs.
18
+ In particular, test that the two following registered jobs run and work as expected:
19
+ - update_dataset_hvd_badge
20
+ - update_dataservice_hvd_badge
21
+ """
22
+ org = OrganizationFactory()
23
+ org.add_badge(PUBLIC_SERVICE)
24
+ org.add_badge(CERTIFIED)
25
+
26
+ datasets = [
27
+ DatasetFactory(organization=org, tags=["hvd"]), # Should be badged HVD
28
+ DatasetFactory(organization=org, tags=["random"]), # Should not be badged HVD
29
+ DatasetFactory(
30
+ organization=org,
31
+ tags=[],
32
+ badges=[Dataset.badges.field.document_type(kind=HVD)],
33
+ ), # Badge should be remove
34
+ ]
35
+ dataservices = [
36
+ DataserviceFactory(organization=org, tags=["hvd"]), # Should be badged HVD
37
+ DataserviceFactory(organization=org, tags=["random"]), # Should not be badged HVD
38
+ DataserviceFactory(
39
+ organization=org,
40
+ tags=[],
41
+ badges=[Dataservice.badges.field.document_type(kind=HVD)],
42
+ ), # Badge should be remove
43
+ ]
44
+
45
+ update_badges.run()
46
+
47
+ [model.reload() for model in (*datasets, *dataservices)]
48
+
49
+ assert datasets[0].badges[0].kind == HVD
50
+ assert datasets[1].badges == []
51
+ assert datasets[2].badges == []
52
+
53
+ assert dataservices[0].badges[0].kind == HVD
54
+ assert dataservices[1].badges == []
55
+ assert dataservices[2].badges == []
@@ -0,0 +1 @@
1
+ HVD = "hvd"
@@ -9,6 +9,14 @@ CONTACT_ROLES = {
9
9
  "contact": _("Contact"),
10
10
  "creator": _("Creator"),
11
11
  "publisher": _("Publisher"),
12
+ "rightsHolder": _("Rights Holder"),
13
+ "custodian": _("Custodian"),
14
+ "distributor": _("Distributor"),
15
+ "originator": _("Originator"),
16
+ "principalInvestigator": _("Principal Investigator"),
17
+ "processor": _("Processor"),
18
+ "resourceProvider": _("Resource Provider"),
19
+ "user": _("User"),
12
20
  }
13
21
 
14
22
 
@@ -9,13 +9,13 @@ from flask_login import current_user
9
9
  from udata.api import API, api, fields
10
10
  from udata.api_fields import patch
11
11
  from udata.auth import admin_permission
12
- from udata.core.dataservices.constants import DATASERVICE_ACCESS_TYPE_RESTRICTED
12
+ from udata.core.access_type.constants import AccessType
13
13
  from udata.core.dataset.models import Dataset
14
14
  from udata.core.followers.api import FollowAPI
15
- from udata.core.site.models import current_site
16
15
  from udata.frontend.markdown import md
17
16
  from udata.i18n import gettext as _
18
17
  from udata.rdf import RDF_EXTENSIONS, graph_response, negociate_content
18
+ from udata.utils import get_rss_feed_list
19
19
 
20
20
  from .models import Dataservice
21
21
  from .rdf import dataservice_to_rdf
@@ -48,7 +48,7 @@ class DataservicesAPI(API):
48
48
  dataservice = patch(Dataservice(), request)
49
49
  if not dataservice.owner and not dataservice.organization:
50
50
  dataservice.owner = current_user._get_current_object()
51
- if dataservice.access_type != DATASERVICE_ACCESS_TYPE_RESTRICTED:
51
+ if dataservice.access_type != AccessType.RESTRICTED:
52
52
  dataservice.access_audiences = []
53
53
  dataservice.save()
54
54
  return dataservice, 201
@@ -62,9 +62,7 @@ class DataservicesAtomFeedAPI(API):
62
62
  _("Latest APIs"), description=None, feed_url=request.url, link=request.url_root
63
63
  )
64
64
 
65
- dataservices: list[Dataservice] = (
66
- Dataservice.objects.visible().order_by("-created_at").limit(current_site.feed_size)
67
- )
65
+ dataservices = get_rss_feed_list(Dataservice.objects.visible(), "created_at")
68
66
  for dataservice in dataservices:
69
67
  author_name = None
70
68
  author_uri = None
@@ -117,7 +115,7 @@ class DataserviceAPI(API):
117
115
 
118
116
  patch(dataservice, request)
119
117
  dataservice.metadata_modified_at = datetime.utcnow()
120
- if dataservice.access_type != DATASERVICE_ACCESS_TYPE_RESTRICTED:
118
+ if dataservice.access_type != AccessType.RESTRICTED:
121
119
  dataservice.access_audiences = []
122
120
 
123
121
  dataservice.save()
@@ -238,19 +236,19 @@ class DataserviceDatasetAPI(API):
238
236
  class DataserviceRdfAPI(API):
239
237
  @api.doc("rdf_dataservice")
240
238
  def get(self, dataservice):
241
- format = RDF_EXTENSIONS[negociate_content()]
242
- url = url_for("api.dataservice_rdf_format", dataservice=dataservice.id, format=format)
239
+ _format = RDF_EXTENSIONS[negociate_content()]
240
+ url = url_for("api.dataservice_rdf_format", dataservice=dataservice.id, _format=_format)
243
241
  return redirect(url)
244
242
 
245
243
 
246
244
  @ns.route(
247
- "/<dataservice:dataservice>/rdf.<format>", endpoint="dataservice_rdf_format", doc=common_doc
245
+ "/<dataservice:dataservice>/rdf.<_format>", endpoint="dataservice_rdf_format", doc=common_doc
248
246
  )
249
247
  @api.response(404, "Dataservice not found")
250
248
  @api.response(410, "Dataservice has been deleted")
251
249
  class DataserviceRdfFormatAPI(API):
252
250
  @api.doc("rdf_dataservice_format")
253
- def get(self, dataservice: Dataservice, format):
251
+ def get(self, dataservice: Dataservice, _format):
254
252
  if not dataservice.permissions["edit"].can():
255
253
  if dataservice.private:
256
254
  api.abort(404)
@@ -260,7 +258,7 @@ class DataserviceRdfFormatAPI(API):
260
258
  resource = dataservice_to_rdf(dataservice)
261
259
  # bypass flask-restplus make_response, since graph_response
262
260
  # is handling the content negociation directly
263
- return make_response(*graph_response(resource, format))
261
+ return make_response(*graph_response(resource, _format))
264
262
 
265
263
 
266
264
  @ns.route("/<id>/followers/", endpoint="dataservice_followers")
@@ -2,7 +2,8 @@ from flask import request
2
2
 
3
3
  from udata import search
4
4
  from udata.api import API, apiv2
5
- from udata.core.dataservices.models import AccessAudience, Dataservice, HarvestMetadata
5
+ from udata.core.access_type.models import AccessAudience
6
+ from udata.core.dataservices.models import Dataservice, HarvestMetadata
6
7
  from udata.utils import multi_to_dict
7
8
 
8
9
  from .models import dataservice_permissions_fields
@@ -11,6 +12,7 @@ from .search import DataserviceSearch
11
12
  apiv2.inherit("DataservicePermissions", dataservice_permissions_fields)
12
13
  apiv2.inherit("DataservicePage", Dataservice.__page_fields__)
13
14
  apiv2.inherit("Dataservice (read)", Dataservice.__read_fields__)
15
+ apiv2.inherit("DataserviceReference", Dataservice.__ref_fields__)
14
16
  apiv2.inherit("HarvestMetadata (read)", HarvestMetadata.__read_fields__)
15
17
  apiv2.inherit("AccessAudience (read)", AccessAudience.__read_fields__)
16
18
 
@@ -1,30 +1 @@
1
1
  DATASERVICE_FORMATS = ["REST", "WMS", "WSL"]
2
-
3
-
4
- DATASERVICE_ACCESS_TYPE_OPEN = "open"
5
- DATASERVICE_ACCESS_TYPE_OPEN_WITH_ACCOUNT = "open_with_account"
6
- DATASERVICE_ACCESS_TYPE_RESTRICTED = "restricted"
7
- DATASERVICE_ACCESS_TYPES = [
8
- DATASERVICE_ACCESS_TYPE_OPEN,
9
- DATASERVICE_ACCESS_TYPE_OPEN_WITH_ACCOUNT,
10
- DATASERVICE_ACCESS_TYPE_RESTRICTED,
11
- ]
12
-
13
- DATASERVICE_ACCESS_AUDIENCE_ADMINISTRATION = "local_authority_and_administration"
14
- DATASERVICE_ACCESS_AUDIENCE_COMPANY = "company_and_association"
15
- DATASERVICE_ACCESS_AUDIENCE_PRIVATE = "private"
16
-
17
- DATASERVICE_ACCESS_AUDIENCE_TYPES = [
18
- DATASERVICE_ACCESS_AUDIENCE_ADMINISTRATION,
19
- DATASERVICE_ACCESS_AUDIENCE_COMPANY,
20
- DATASERVICE_ACCESS_AUDIENCE_PRIVATE,
21
- ]
22
-
23
- DATASERVICE_ACCESS_AUDIENCE_YES = "yes"
24
- DATASERVICE_ACCESS_AUDIENCE_NO = "no"
25
- DATASERVICE_ACCESS_AUDIENCE_UNDER_CONDITIONS = "under_condition"
26
- DATASERVICE_ACCESS_AUDIENCE_CONDITIONS = [
27
- DATASERVICE_ACCESS_AUDIENCE_YES,
28
- DATASERVICE_ACCESS_AUDIENCE_NO,
29
- DATASERVICE_ACCESS_AUDIENCE_UNDER_CONDITIONS,
30
- ]
@@ -2,19 +2,17 @@ from datetime import datetime
2
2
 
3
3
  from blinker import Signal
4
4
  from flask import url_for
5
+ from flask_babel import LazyString
5
6
  from mongoengine import Q
6
7
  from mongoengine.signals import post_save
7
8
 
8
9
  import udata.core.contact_point.api_fields as contact_api_fields
9
10
  from udata.api import api, fields
10
11
  from udata.api_fields import field, generate_fields
12
+ from udata.core.access_type.models import WithAccessType
11
13
  from udata.core.activity.models import Auditable
12
- from udata.core.dataservices.constants import (
13
- DATASERVICE_ACCESS_AUDIENCE_CONDITIONS,
14
- DATASERVICE_ACCESS_AUDIENCE_TYPES,
15
- DATASERVICE_ACCESS_TYPES,
16
- DATASERVICE_FORMATS,
17
- )
14
+ from udata.core.constants import HVD
15
+ from udata.core.dataservices.constants import DATASERVICE_FORMATS
18
16
  from udata.core.dataset.api_fields import dataset_ref_fields
19
17
  from udata.core.dataset.models import Dataset
20
18
  from udata.core.linkable import Linkable
@@ -22,19 +20,12 @@ from udata.core.metrics.helpers import get_stock_metrics
22
20
  from udata.core.metrics.models import WithMetrics
23
21
  from udata.core.owned import Owned, OwnedQuerySet
24
22
  from udata.i18n import lazy_gettext as _
25
- from udata.models import Discussion, Follow, db
26
- from udata.mongo.errors import FieldValidationError
23
+ from udata.models import Badge, BadgeMixin, BadgesList, Discussion, Follow, db
27
24
  from udata.uris import cdata_url
28
25
 
29
- # "frequency"
30
- # "harvest"
31
- # "internal"
32
- # "page"
33
- # "quality" # Peut-être pas dans une v1 car la qualité sera probablement calculé différemment
34
- # "datasets" # objet : liste de datasets liés à une API
35
- # "spatial"
36
- # "temporal_coverage"
37
-
26
+ BADGES: dict[str, LazyString] = {
27
+ HVD: _("Dataservice serving high value datasets"),
28
+ }
38
29
 
39
30
  dataservice_permissions_fields = api.model(
40
31
  "DataservicePermissions",
@@ -89,6 +80,20 @@ class DataserviceQuerySet(OwnedQuerySet):
89
80
  return self(dataservices_filter)
90
81
 
91
82
 
83
+ def validate_badge(value):
84
+ if value not in Dataservice.__badges__.keys():
85
+ raise db.ValidationError("Unknown badge type")
86
+
87
+
88
+ class DataserviceBadge(Badge):
89
+ kind = db.StringField(required=True, validation=validate_badge)
90
+
91
+
92
+ class DataserviceBadgeMixin(BadgeMixin):
93
+ badges = field(BadgesList(DataserviceBadge), **BadgeMixin.default_badges_list_params)
94
+ __badges__ = BADGES
95
+
96
+
92
97
  @generate_fields()
93
98
  class HarvestMetadata(db.EmbeddedDocument):
94
99
  backend = field(db.StringField())
@@ -119,28 +124,13 @@ class HarvestMetadata(db.EmbeddedDocument):
119
124
  archived_reason = field(db.StringField())
120
125
 
121
126
 
122
- @generate_fields()
123
- class AccessAudience(db.EmbeddedDocument):
124
- role = field(db.StringField(choices=DATASERVICE_ACCESS_AUDIENCE_TYPES), filterable={})
125
- condition = field(db.StringField(choices=DATASERVICE_ACCESS_AUDIENCE_CONDITIONS), filterable={})
126
-
127
-
128
- def check_only_one_condition_per_role(access_audiences, **_kwargs):
129
- roles = set(e["role"] for e in access_audiences)
130
- if len(roles) != len(access_audiences):
131
- raise FieldValidationError(
132
- _("You can only set one condition for a given access audience role"),
133
- field="access_audiences",
134
- )
135
-
136
-
137
127
  def filter_by_topic(base_query, filter_value):
138
128
  from udata.core.topic.models import Topic
139
129
 
140
130
  try:
141
131
  topic = Topic.objects.get(id=filter_value)
142
132
  except Topic.DoesNotExist:
143
- pass
133
+ return base_query
144
134
  else:
145
135
  return base_query.filter(
146
136
  id__in=[
@@ -150,18 +140,32 @@ def filter_by_topic(base_query, filter_value):
150
140
  )
151
141
 
152
142
 
143
+ def filter_by_reuse(base_query, filter_value):
144
+ from udata.core.reuse.models import Reuse
145
+
146
+ try:
147
+ reuse = Reuse.objects.get(id=filter_value)
148
+ except Reuse.DoesNotExist:
149
+ return base_query
150
+ else:
151
+ return base_query.filter(id__in=[dataservice.id for dataservice in reuse.dataservices])
152
+
153
+
153
154
  @generate_fields(
154
155
  searchable=True,
155
156
  nested_filters={"organization_badge": "organization.badges"},
156
157
  standalone_filters=[
157
- {"key": "topic", "constraints": "objectid", "query": filter_by_topic, "type": str}
158
+ {"key": "topic", "constraints": ["objectid"], "query": filter_by_topic, "type": str},
159
+ {"key": "reuse", "constraints": ["objectid"], "query": filter_by_reuse, "type": str},
158
160
  ],
159
161
  additional_sorts=[
160
162
  {"key": "followers", "value": "metrics.followers"},
161
163
  {"key": "views", "value": "metrics.views"},
162
164
  ],
163
165
  )
164
- class Dataservice(Auditable, WithMetrics, Linkable, Owned, db.Document):
166
+ class Dataservice(
167
+ Auditable, WithMetrics, WithAccessType, DataserviceBadgeMixin, Linkable, Owned, db.Document
168
+ ):
165
169
  meta = {
166
170
  "indexes": [
167
171
  "$title",
@@ -212,14 +216,6 @@ class Dataservice(Auditable, WithMetrics, Linkable, Owned, db.Document):
212
216
  availability = field(db.FloatField(min=0, max=100), example="99.99")
213
217
  availability_url = field(db.URLField())
214
218
 
215
- access_type = field(db.StringField(choices=DATASERVICE_ACCESS_TYPES), filterable={})
216
- access_audiences = field(
217
- db.EmbeddedDocumentListField(AccessAudience),
218
- checks=[check_only_one_condition_per_role],
219
- )
220
-
221
- authorization_request_url = field(db.URLField())
222
-
223
219
  format = field(db.StringField(choices=DATASERVICE_FORMATS))
224
220
 
225
221
  license = field(
@@ -303,7 +299,7 @@ class Dataservice(Auditable, WithMetrics, Linkable, Owned, db.Document):
303
299
  auditable=False,
304
300
  )
305
301
 
306
- @field(description="Link to the API endpoint for this dataservice")
302
+ @field(description="Link to the API endpoint for this dataservice", show_as_ref=True)
307
303
  def self_api_url(self, **kwargs):
308
304
  return url_for(
309
305
  "api.dataservice",
@@ -323,6 +319,10 @@ class Dataservice(Auditable, WithMetrics, Linkable, Owned, db.Document):
323
319
  "views",
324
320
  ]
325
321
 
322
+ @property
323
+ def is_visible(self):
324
+ return not self.is_hidden
325
+
326
326
  @property
327
327
  def is_hidden(self):
328
328
  return self.private or self.deleted_at or self.archived_at
@@ -1,6 +1,7 @@
1
1
  from flask import current_app
2
2
  from rdflib import RDF, BNode, Graph, Literal, URIRef
3
3
 
4
+ from udata.core.constants import HVD
4
5
  from udata.core.dataservices.models import Dataservice
5
6
  from udata.core.dataservices.models import HarvestMetadata as HarvestDataserviceMetadata
6
7
  from udata.core.dataset.models import Dataset, License
@@ -146,7 +147,7 @@ def dataservice_to_rdf(dataservice: Dataservice, graph=None):
146
147
 
147
148
  # Add DCAT-AP HVD properties if the dataservice is tagged hvd.
148
149
  # See https://semiceu.github.io/DCAT-AP/releases/2.2.0-hvd/
149
- is_hvd = current_app.config["HVD_SUPPORT"] and "hvd" in dataservice.tags
150
+ is_hvd = current_app.config["HVD_SUPPORT"] and any(b.kind == HVD for b in dataservice.badges)
150
151
  if is_hvd:
151
152
  d.add(DCATAP.applicableLegislation, URIRef(HVD_LEGISLATION))
152
153
 
@@ -5,11 +5,7 @@ from flask_restx.inputs import boolean
5
5
 
6
6
  from udata.api import api
7
7
  from udata.api.parsers import ModelApiParser
8
- from udata.core.dataservices.constants import (
9
- DATASERVICE_ACCESS_TYPE_OPEN,
10
- DATASERVICE_ACCESS_TYPE_OPEN_WITH_ACCOUNT,
11
- DATASERVICE_ACCESS_TYPE_RESTRICTED,
12
- )
8
+ from udata.core.access_type.constants import AccessType
13
9
  from udata.models import Dataservice, Organization, User
14
10
  from udata.search import (
15
11
  BoolFilter,
@@ -54,9 +50,9 @@ class DataserviceApiParser(ModelApiParser):
54
50
  dataservices = dataservices.filter(organization=args["organization"])
55
51
  if "is_restricted" in args:
56
52
  dataservices = dataservices.filter(
57
- access_type__in=[DATASERVICE_ACCESS_TYPE_RESTRICTED]
53
+ access_type__in=[AccessType.RESTRICTED]
58
54
  if boolean(args["is_restricted"])
59
- else [DATASERVICE_ACCESS_TYPE_OPEN, DATASERVICE_ACCESS_TYPE_OPEN_WITH_ACCOUNT]
55
+ else [AccessType.OPEN, AccessType.OPEN_WITH_ACCOUNT]
60
56
  )
61
57
  if args.get("featured"):
62
58
  dataservices = dataservices.filter(featured=args["featured"])
@@ -79,7 +75,7 @@ class DataserviceSearch(ModelSearchAdapter):
79
75
 
80
76
  @classmethod
81
77
  def is_indexable(cls, dataservice: Dataservice) -> bool:
82
- return dataservice.deleted_at is None and not dataservice.private
78
+ return dataservice.is_visible
83
79
 
84
80
  @classmethod
85
81
  def mongo_search(cls, args):
@@ -126,6 +122,6 @@ class DataserviceSearch(ModelSearchAdapter):
126
122
  "tags": dataservice.tags,
127
123
  "extras": extras,
128
124
  "followers": dataservice.metrics.get("followers", 0),
129
- "is_restricted": dataservice.access_type == DATASERVICE_ACCESS_TYPE_RESTRICTED,
125
+ "is_restricted": dataservice.access_type == AccessType.RESTRICTED,
130
126
  "views": dataservice.metrics.get("views", 0),
131
127
  }
@@ -1,6 +1,11 @@
1
1
  from celery.utils.log import get_task_logger
2
+ from flask import current_app
2
3
 
4
+ from udata.core.badges import tasks as badge_tasks
5
+ from udata.core.constants import HVD
3
6
  from udata.core.dataservices.models import Dataservice
7
+ from udata.core.organization.constants import CERTIFIED, PUBLIC_SERVICE
8
+ from udata.core.organization.models import Organization
4
9
  from udata.core.topic.models import TopicElement
5
10
  from udata.harvest.models import HarvestJob
6
11
  from udata.models import Discussion, Follow, Transfer
@@ -25,3 +30,31 @@ def purge_dataservices(self):
25
30
  TopicElement.objects(element=dataservice).update(element=None)
26
31
  # Remove dataservice
27
32
  dataservice.delete()
33
+
34
+
35
+ @badge_tasks.register(model=Dataservice, badge=HVD)
36
+ def update_dataservice_hvd_badge() -> None:
37
+ """
38
+ Update HVD badges to candidate dataservices, based on the hvd tag.
39
+ Only dataservices owned by certified and public service organizations are candidate to have a HVD badge.
40
+ """
41
+ if not current_app.config["HVD_SUPPORT"]:
42
+ log.error("You need to set HVD_SUPPORT if you want to update dataservice hvd badge")
43
+ return
44
+ public_certified_orgs = (
45
+ Organization.objects(badges__kind=PUBLIC_SERVICE).filter(badges__kind=CERTIFIED).only("id")
46
+ )
47
+
48
+ dataservices = Dataservice.objects(
49
+ tags="hvd", badges__kind__ne="hvd", organization__in=public_certified_orgs
50
+ )
51
+ log.info(f"Adding HVD badge to {dataservices.count()} dataservices")
52
+ for dataservice in dataservices:
53
+ dataservice.add_badge(HVD)
54
+
55
+ dataservices = Dataservice.objects(
56
+ tags__nin=["hvd"], badges__kind="hvd", organization__in=public_certified_orgs
57
+ )
58
+ log.info(f"Remove HVD badge from {dataservices.count()} dataservices")
59
+ for dataservice in dataservices:
60
+ dataservice.remove_badge(HVD)
udata/core/dataset/api.py CHANGED
@@ -41,14 +41,12 @@ from udata.core.followers.api import FollowAPI
41
41
  from udata.core.followers.models import Follow
42
42
  from udata.core.organization.models import Organization
43
43
  from udata.core.reuse.models import Reuse
44
- from udata.core.site.models import current_site
45
44
  from udata.core.storages.api import handle_upload, upload_parser
46
45
  from udata.core.topic.models import Topic
47
46
  from udata.frontend.markdown import md
48
47
  from udata.i18n import gettext as _
49
- from udata.linkchecker.checker import check_resource
50
48
  from udata.rdf import RDF_EXTENSIONS, graph_response, negociate_content
51
- from udata.utils import get_by
49
+ from udata.utils import get_by, get_rss_feed_list
52
50
 
53
51
  from .api_fields import (
54
52
  catalog_schema_fields,
@@ -291,6 +289,12 @@ community_parser.add_argument(
291
289
 
292
290
  common_doc = {"params": {"dataset": "The dataset ID or slug"}}
293
291
 
292
+ # Build catalog_parser from DatasetApiParser parser with a default page_size of 100
293
+ catalog_parser = DatasetApiParser().parser
294
+ catalog_parser.replace_argument(
295
+ "page_size", type=int, location="args", default=100, help="The page size"
296
+ )
297
+
294
298
 
295
299
  @ns.route("/", endpoint="datasets")
296
300
  class DatasetListAPI(API):
@@ -331,9 +335,10 @@ class DatasetsAtomFeedAPI(API):
331
335
  link=request.url_root,
332
336
  )
333
337
 
334
- datasets: list[Dataset] = (
335
- Dataset.objects.visible().order_by("-created_at_internal").limit(current_site.feed_size)
338
+ datasets: list[Dataset] = get_rss_feed_list(
339
+ Dataset.objects.visible(), "created_at_internal"
336
340
  )
341
+
337
342
  for dataset in datasets:
338
343
  author_name = None
339
344
  author_uri = None
@@ -432,17 +437,17 @@ class DatasetFeaturedAPI(API):
432
437
  class DatasetRdfAPI(API):
433
438
  @api.doc("rdf_dataset")
434
439
  def get(self, dataset):
435
- format = RDF_EXTENSIONS[negociate_content()]
436
- url = url_for("api.dataset_rdf_format", dataset=dataset.id, format=format)
440
+ _format = RDF_EXTENSIONS[negociate_content()]
441
+ url = url_for("api.dataset_rdf_format", dataset=dataset.id, _format=_format)
437
442
  return redirect(url)
438
443
 
439
444
 
440
- @ns.route("/<dataset:dataset>/rdf.<format>", endpoint="dataset_rdf_format", doc=common_doc)
445
+ @ns.route("/<dataset:dataset>/rdf.<_format>", endpoint="dataset_rdf_format", doc=common_doc)
441
446
  @api.response(404, "Dataset not found")
442
447
  @api.response(410, "Dataset has been deleted")
443
448
  class DatasetRdfFormatAPI(API):
444
449
  @api.doc("rdf_dataset_format")
445
- def get(self, dataset, format):
450
+ def get(self, dataset, _format):
446
451
  if not dataset.permissions["edit"].can():
447
452
  if dataset.private:
448
453
  api.abort(404)
@@ -452,7 +457,7 @@ class DatasetRdfFormatAPI(API):
452
457
  resource = dataset_to_rdf(dataset)
453
458
  # bypass flask-restplus make_response, since graph_response
454
459
  # is handling the content negociation directly
455
- return make_response(*graph_response(resource, format))
460
+ return make_response(*graph_response(resource, _format))
456
461
 
457
462
 
458
463
  @ns.route("/badges/", endpoint="available_dataset_badges")
@@ -902,20 +907,6 @@ class AllowedExtensionsAPI(API):
902
907
  return sorted(current_app.config["ALLOWED_RESOURCES_EXTENSIONS"])
903
908
 
904
909
 
905
- @ns.route(
906
- "/<dataset:dataset>/resources/<uuid:rid>/check/",
907
- endpoint="check_dataset_resource",
908
- doc=common_doc,
909
- )
910
- @api.param("rid", "The resource unique identifier")
911
- class CheckDatasetResource(API, ResourceMixin):
912
- @api.doc("check_dataset_resource")
913
- def get(self, dataset, rid):
914
- """Checks that a resource's URL exists and returns metadata."""
915
- resource = self.get_resource_or_404(dataset, rid)
916
- return check_resource(resource)
917
-
918
-
919
910
  @ns.route("/resource_types/", endpoint="resource_types")
920
911
  class ResourceTypesAPI(API):
921
912
  @api.doc("resource_types")