udata 12.0.2.dev15__py3-none-any.whl → 13.0.1.dev21__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of udata might be problematic. Click here for more details.

Files changed (258) hide show
  1. udata/api/__init__.py +1 -0
  2. udata/api_fields.py +10 -4
  3. udata/app.py +11 -10
  4. udata/auth/__init__.py +9 -10
  5. udata/auth/mails.py +137 -45
  6. udata/auth/views.py +5 -12
  7. udata/commands/__init__.py +2 -3
  8. udata/commands/info.py +1 -3
  9. udata/commands/tests/test_fixtures.py +6 -3
  10. udata/core/access_type/api.py +18 -0
  11. udata/core/access_type/constants.py +98 -0
  12. udata/core/access_type/models.py +44 -0
  13. udata/core/activity/models.py +1 -1
  14. udata/core/badges/models.py +1 -1
  15. udata/core/badges/tasks.py +35 -1
  16. udata/core/badges/tests/test_commands.py +2 -4
  17. udata/core/badges/tests/test_model.py +2 -2
  18. udata/core/badges/tests/test_tasks.py +55 -0
  19. udata/core/constants.py +1 -0
  20. udata/core/contact_point/models.py +8 -0
  21. udata/core/dataservices/api.py +3 -3
  22. udata/core/dataservices/apiv2.py +3 -1
  23. udata/core/dataservices/constants.py +0 -29
  24. udata/core/dataservices/models.py +44 -44
  25. udata/core/dataservices/rdf.py +2 -1
  26. udata/core/dataservices/search.py +5 -9
  27. udata/core/dataservices/tasks.py +33 -0
  28. udata/core/dataset/api_fields.py +11 -0
  29. udata/core/dataset/apiv2.py +11 -0
  30. udata/core/dataset/constants.py +0 -1
  31. udata/core/dataset/forms.py +29 -0
  32. udata/core/dataset/models.py +16 -4
  33. udata/core/dataset/rdf.py +2 -1
  34. udata/core/dataset/search.py +2 -2
  35. udata/core/dataset/tasks.py +86 -8
  36. udata/core/discussions/mails.py +63 -0
  37. udata/core/discussions/tasks.py +4 -18
  38. udata/core/metrics/__init__.py +0 -6
  39. udata/core/organization/api.py +3 -1
  40. udata/core/organization/mails.py +144 -0
  41. udata/core/organization/models.py +2 -1
  42. udata/core/organization/search.py +1 -1
  43. udata/core/organization/tasks.py +21 -49
  44. udata/core/pages/tests/test_api.py +0 -2
  45. udata/core/reuse/api.py +27 -1
  46. udata/core/reuse/mails.py +21 -0
  47. udata/core/reuse/models.py +10 -1
  48. udata/core/reuse/search.py +1 -1
  49. udata/core/reuse/tasks.py +2 -3
  50. udata/core/site/models.py +2 -6
  51. udata/core/spatial/tests/test_api.py +17 -20
  52. udata/core/spatial/tests/test_models.py +3 -3
  53. udata/core/user/mails.py +54 -0
  54. udata/core/user/models.py +2 -3
  55. udata/core/user/tasks.py +8 -23
  56. udata/core/user/tests/test_user_model.py +2 -6
  57. udata/entrypoints.py +0 -5
  58. udata/features/identicon/tests/test_backends.py +3 -13
  59. udata/forms/fields.py +3 -3
  60. udata/forms/widgets.py +2 -2
  61. udata/frontend/__init__.py +3 -32
  62. udata/harvest/actions.py +4 -9
  63. udata/harvest/api.py +5 -14
  64. udata/harvest/backends/__init__.py +20 -11
  65. udata/harvest/backends/base.py +2 -2
  66. udata/harvest/backends/ckan/harvesters.py +2 -1
  67. udata/harvest/backends/dcat.py +3 -0
  68. udata/harvest/backends/maaf.py +1 -0
  69. udata/harvest/commands.py +6 -4
  70. udata/harvest/forms.py +9 -6
  71. udata/harvest/tasks.py +3 -5
  72. udata/harvest/tests/ckan/test_ckan_backend.py +300 -337
  73. udata/harvest/tests/ckan/test_ckan_backend_errors.py +94 -99
  74. udata/harvest/tests/ckan/test_ckan_backend_filters.py +128 -122
  75. udata/harvest/tests/ckan/test_dkan_backend.py +39 -51
  76. udata/harvest/tests/dcat/datara--5a26b0f6-0ccf-46ad-ac58-734054b91977.rdf.xml +255 -0
  77. udata/harvest/tests/dcat/datara--f40c3860-7236-4b30-a141-23b8ae33f7b2.rdf.xml +289 -0
  78. udata/harvest/tests/factories.py +1 -1
  79. udata/harvest/tests/test_actions.py +11 -9
  80. udata/harvest/tests/test_api.py +4 -5
  81. udata/harvest/tests/test_base_backend.py +5 -4
  82. udata/harvest/tests/test_dcat_backend.py +50 -19
  83. udata/harvest/tests/test_models.py +2 -4
  84. udata/harvest/tests/test_notifications.py +2 -4
  85. udata/harvest/tests/test_tasks.py +2 -3
  86. udata/mail.py +90 -53
  87. udata/migrations/2025-01-05-dataservices-fields-changes.py +8 -14
  88. udata/migrations/2025-10-21-remove-ckan-harvest-modified-at.py +28 -0
  89. udata/migrations/2025-10-29-harvesters-sources-integrity.py +27 -0
  90. udata/mongo/taglist_field.py +3 -3
  91. udata/rdf.py +32 -15
  92. udata/sentry.py +3 -4
  93. udata/settings.py +7 -2
  94. udata/tags.py +5 -5
  95. udata/tasks.py +3 -3
  96. udata/templates/mail/message.html +65 -0
  97. udata/templates/mail/message.txt +16 -0
  98. udata/tests/__init__.py +40 -58
  99. udata/tests/api/__init__.py +87 -2
  100. udata/tests/api/test_activities_api.py +17 -23
  101. udata/tests/api/test_auth_api.py +2 -4
  102. udata/tests/api/test_contact_points.py +48 -54
  103. udata/tests/api/test_dataservices_api.py +57 -37
  104. udata/tests/api/test_datasets_api.py +146 -49
  105. udata/tests/api/test_me_api.py +4 -6
  106. udata/tests/api/test_organizations_api.py +19 -38
  107. udata/tests/api/test_reports_api.py +0 -4
  108. udata/tests/api/test_reuses_api.py +92 -19
  109. udata/tests/api/test_security_api.py +124 -0
  110. udata/tests/api/test_swagger.py +2 -3
  111. udata/tests/api/test_tags_api.py +6 -7
  112. udata/tests/api/test_transfer_api.py +0 -2
  113. udata/tests/api/test_user_api.py +8 -10
  114. udata/tests/apiv2/test_datasets.py +0 -4
  115. udata/tests/apiv2/test_me_api.py +0 -2
  116. udata/tests/apiv2/test_organizations.py +0 -2
  117. udata/tests/apiv2/test_swagger.py +2 -3
  118. udata/tests/apiv2/test_topics.py +0 -2
  119. udata/tests/cli/test_cli_base.py +14 -12
  120. udata/tests/cli/test_db_cli.py +51 -54
  121. udata/tests/contact_point/test_contact_point_models.py +2 -2
  122. udata/tests/dataservice/test_csv_adapter.py +2 -5
  123. udata/tests/dataservice/test_dataservice_rdf.py +8 -6
  124. udata/tests/dataservice/test_dataservice_tasks.py +36 -38
  125. udata/tests/dataset/test_csv_adapter.py +2 -5
  126. udata/tests/dataset/test_dataset_actions.py +2 -4
  127. udata/tests/dataset/test_dataset_commands.py +2 -4
  128. udata/tests/dataset/test_dataset_events.py +3 -3
  129. udata/tests/dataset/test_dataset_model.py +6 -7
  130. udata/tests/dataset/test_dataset_rdf.py +201 -12
  131. udata/tests/dataset/test_dataset_recommendations.py +2 -2
  132. udata/tests/dataset/test_dataset_tasks.py +66 -68
  133. udata/tests/dataset/test_resource_preview.py +39 -48
  134. udata/tests/dataset/test_transport_tasks.py +2 -2
  135. udata/tests/features/territories/__init__.py +0 -6
  136. udata/tests/features/territories/test_territories_api.py +25 -24
  137. udata/tests/forms/test_current_user_field.py +2 -2
  138. udata/tests/forms/test_dict_field.py +2 -4
  139. udata/tests/forms/test_extras_fields.py +2 -3
  140. udata/tests/forms/test_image_field.py +2 -2
  141. udata/tests/forms/test_model_field.py +2 -4
  142. udata/tests/forms/test_publish_as_field.py +2 -4
  143. udata/tests/forms/test_user_forms.py +26 -29
  144. udata/tests/frontend/test_auth.py +2 -3
  145. udata/tests/frontend/test_csv.py +5 -6
  146. udata/tests/frontend/test_error_handlers.py +2 -3
  147. udata/tests/frontend/test_hooks.py +5 -7
  148. udata/tests/frontend/test_markdown.py +3 -4
  149. udata/tests/helpers.py +2 -7
  150. udata/tests/metrics/test_metrics.py +52 -48
  151. udata/tests/metrics/test_tasks.py +154 -150
  152. udata/tests/organization/test_csv_adapter.py +2 -5
  153. udata/tests/organization/test_notifications.py +2 -4
  154. udata/tests/organization/test_organization_model.py +3 -4
  155. udata/tests/organization/test_organization_rdf.py +2 -8
  156. udata/tests/plugin.py +6 -110
  157. udata/tests/reuse/test_reuse_model.py +3 -4
  158. udata/tests/site/test_site_api.py +0 -2
  159. udata/tests/site/test_site_csv_exports.py +0 -2
  160. udata/tests/site/test_site_metrics.py +2 -4
  161. udata/tests/site/test_site_model.py +2 -2
  162. udata/tests/site/test_site_rdf.py +4 -7
  163. udata/tests/test_activity.py +3 -3
  164. udata/tests/test_api_fields.py +6 -9
  165. udata/tests/test_cors.py +0 -2
  166. udata/tests/test_dcat_commands.py +2 -3
  167. udata/tests/test_discussions.py +2 -7
  168. udata/tests/test_mail.py +150 -114
  169. udata/tests/test_migrations.py +413 -419
  170. udata/tests/test_model.py +10 -11
  171. udata/tests/test_notifications.py +2 -3
  172. udata/tests/test_owned.py +3 -3
  173. udata/tests/test_rdf.py +19 -15
  174. udata/tests/test_routing.py +5 -5
  175. udata/tests/test_storages.py +6 -5
  176. udata/tests/test_tags.py +2 -4
  177. udata/tests/test_topics.py +2 -4
  178. udata/tests/test_transfer.py +4 -5
  179. udata/tests/topic/test_topic_tasks.py +25 -27
  180. udata/tests/user/test_user_rdf.py +2 -8
  181. udata/tests/user/test_user_tasks.py +3 -5
  182. udata/tests/workers/test_jobs_commands.py +2 -2
  183. udata/tests/workers/test_tasks_routing.py +27 -27
  184. udata/translations/ar/LC_MESSAGES/udata.mo +0 -0
  185. udata/translations/ar/LC_MESSAGES/udata.po +369 -435
  186. udata/translations/de/LC_MESSAGES/udata.mo +0 -0
  187. udata/translations/de/LC_MESSAGES/udata.po +371 -437
  188. udata/translations/es/LC_MESSAGES/udata.mo +0 -0
  189. udata/translations/es/LC_MESSAGES/udata.po +369 -435
  190. udata/translations/fr/LC_MESSAGES/udata.mo +0 -0
  191. udata/translations/fr/LC_MESSAGES/udata.po +381 -447
  192. udata/translations/it/LC_MESSAGES/udata.mo +0 -0
  193. udata/translations/it/LC_MESSAGES/udata.po +371 -437
  194. udata/translations/pt/LC_MESSAGES/udata.mo +0 -0
  195. udata/translations/pt/LC_MESSAGES/udata.po +371 -437
  196. udata/translations/sr/LC_MESSAGES/udata.mo +0 -0
  197. udata/translations/sr/LC_MESSAGES/udata.po +372 -438
  198. udata/translations/udata.pot +379 -440
  199. udata/utils.py +14 -2
  200. {udata-12.0.2.dev15.dist-info → udata-13.0.1.dev21.dist-info}/METADATA +1 -2
  201. {udata-12.0.2.dev15.dist-info → udata-13.0.1.dev21.dist-info}/RECORD +205 -242
  202. udata/templates/mail/account_deleted.html +0 -5
  203. udata/templates/mail/account_deleted.txt +0 -6
  204. udata/templates/mail/account_inactivity.html +0 -40
  205. udata/templates/mail/account_inactivity.txt +0 -31
  206. udata/templates/mail/badge_added_association.html +0 -33
  207. udata/templates/mail/badge_added_association.txt +0 -11
  208. udata/templates/mail/badge_added_certified.html +0 -33
  209. udata/templates/mail/badge_added_certified.txt +0 -11
  210. udata/templates/mail/badge_added_company.html +0 -33
  211. udata/templates/mail/badge_added_company.txt +0 -11
  212. udata/templates/mail/badge_added_local_authority.html +0 -33
  213. udata/templates/mail/badge_added_local_authority.txt +0 -11
  214. udata/templates/mail/badge_added_public_service.html +0 -33
  215. udata/templates/mail/badge_added_public_service.txt +0 -11
  216. udata/templates/mail/discussion_closed.html +0 -47
  217. udata/templates/mail/discussion_closed.txt +0 -16
  218. udata/templates/mail/inactive_account_deleted.html +0 -5
  219. udata/templates/mail/inactive_account_deleted.txt +0 -6
  220. udata/templates/mail/membership_refused.html +0 -20
  221. udata/templates/mail/membership_refused.txt +0 -11
  222. udata/templates/mail/membership_request.html +0 -46
  223. udata/templates/mail/membership_request.txt +0 -12
  224. udata/templates/mail/new_discussion.html +0 -44
  225. udata/templates/mail/new_discussion.txt +0 -15
  226. udata/templates/mail/new_discussion_comment.html +0 -45
  227. udata/templates/mail/new_discussion_comment.txt +0 -16
  228. udata/templates/mail/new_member.html +0 -27
  229. udata/templates/mail/new_member.txt +0 -11
  230. udata/templates/mail/new_reuse.html +0 -37
  231. udata/templates/mail/new_reuse.txt +0 -9
  232. udata/templates/mail/test.html +0 -6
  233. udata/templates/mail/test.txt +0 -6
  234. udata/templates/mail/user_mail_card.html +0 -26
  235. udata/templates/security/email/base.html +0 -105
  236. udata/templates/security/email/base.txt +0 -6
  237. udata/templates/security/email/button.html +0 -3
  238. udata/templates/security/email/change_notice.html +0 -22
  239. udata/templates/security/email/change_notice.txt +0 -8
  240. udata/templates/security/email/confirmation_instructions.html +0 -20
  241. udata/templates/security/email/confirmation_instructions.txt +0 -7
  242. udata/templates/security/email/login_instructions.html +0 -19
  243. udata/templates/security/email/login_instructions.txt +0 -7
  244. udata/templates/security/email/reset_instructions.html +0 -24
  245. udata/templates/security/email/reset_instructions.txt +0 -9
  246. udata/templates/security/email/reset_notice.html +0 -11
  247. udata/templates/security/email/reset_notice.txt +0 -4
  248. udata/templates/security/email/welcome.html +0 -24
  249. udata/templates/security/email/welcome.txt +0 -9
  250. udata/templates/security/email/welcome_existing.html +0 -32
  251. udata/templates/security/email/welcome_existing.txt +0 -14
  252. udata/terms.md +0 -6
  253. udata/tests/frontend/__init__.py +0 -23
  254. udata/tests/metrics/conftest.py +0 -15
  255. {udata-12.0.2.dev15.dist-info → udata-13.0.1.dev21.dist-info}/WHEEL +0 -0
  256. {udata-12.0.2.dev15.dist-info → udata-13.0.1.dev21.dist-info}/entry_points.txt +0 -0
  257. {udata-12.0.2.dev15.dist-info → udata-13.0.1.dev21.dist-info}/licenses/LICENSE +0 -0
  258. {udata-12.0.2.dev15.dist-info → udata-13.0.1.dev21.dist-info}/top_level.txt +0 -0
@@ -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,7 +9,7 @@ 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
15
  from udata.frontend.markdown import md
@@ -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
@@ -115,7 +115,7 @@ class DataserviceAPI(API):
115
115
 
116
116
  patch(dataservice, request)
117
117
  dataservice.metadata_modified_at = datetime.utcnow()
118
- if dataservice.access_type != DATASERVICE_ACCESS_TYPE_RESTRICTED:
118
+ if dataservice.access_type != AccessType.RESTRICTED:
119
119
  dataservice.access_audiences = []
120
120
 
121
121
  dataservice.save()
@@ -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)
@@ -1,4 +1,5 @@
1
1
  from udata.api import api, base_reference, fields
2
+ from udata.core.access_type.models import AccessAudience
2
3
  from udata.core.badges.fields import badge_fields
3
4
  from udata.core.contact_point.api_fields import contact_point_fields
4
5
  from udata.core.organization.api_fields import org_ref_fields
@@ -287,6 +288,11 @@ DEFAULT_MASK = ",".join(
287
288
  "temporal_coverage",
288
289
  "spatial",
289
290
  "license",
291
+ "access_type",
292
+ "access_audiences",
293
+ "authorization_request_url",
294
+ "access_type_reason_category",
295
+ "access_type_reason",
290
296
  "uri",
291
297
  "page",
292
298
  "last_update",
@@ -394,6 +400,11 @@ dataset_fields = api.model(
394
400
  "license": fields.String(
395
401
  attribute="license.id", default=DEFAULT_LICENSE["id"], description="The dataset license"
396
402
  ),
403
+ "access_type": fields.String(allow_null=True),
404
+ "access_audiences": fields.List(fields.Nested(AccessAudience.__read_fields__)),
405
+ "authorization_request_url": fields.String(allow_null=True),
406
+ "access_type_reason_category": fields.String(allow_null=True),
407
+ "access_type_reason": fields.String(allow_null=True),
397
408
  "uri": fields.String(
398
409
  attribute=lambda d: d.self_api_url(),
399
410
  description="The API URI for this dataset",
@@ -7,6 +7,7 @@ from flask_restx import marshal
7
7
 
8
8
  from udata import search
9
9
  from udata.api import API, apiv2, fields
10
+ from udata.core.access_type.models import AccessAudience
10
11
  from udata.core.contact_point.api_fields import contact_point_fields
11
12
  from udata.core.dataset.api_fields import license_fields
12
13
  from udata.core.organization.api_fields import member_user_with_email_fields
@@ -62,6 +63,11 @@ DEFAULT_MASK_APIV2 = ",".join(
62
63
  "temporal_coverage",
63
64
  "spatial",
64
65
  "license",
66
+ "access_type",
67
+ "access_audiences",
68
+ "access_type_reason_category",
69
+ "access_type_reason",
70
+ "authorization_request_url",
65
71
  "uri",
66
72
  "page",
67
73
  "last_update",
@@ -202,6 +208,11 @@ dataset_fields = apiv2.model(
202
208
  default=DEFAULT_LICENSE["id"],
203
209
  description="The dataset license (full License object if `X-Get-Datasets-Full-Objects` is set, ID of the license otherwise)",
204
210
  ),
211
+ "access_type": fields.String(allow_null=True),
212
+ "access_audiences": fields.Nested(AccessAudience.__read_fields__),
213
+ "authorization_request_url": fields.String(allow_null=True),
214
+ "access_type_reason_category": fields.String(allow_null=True),
215
+ "access_type_reason": fields.String(allow_null=True),
205
216
  "uri": fields.String(
206
217
  attribute=lambda d: d.self_api_url(),
207
218
  description="The API URI for this dataset",
@@ -166,7 +166,6 @@ DEFAULT_CHECKSUM_TYPE = "sha1"
166
166
  PIVOTAL_DATA = "pivotal-data"
167
167
  SPD = "spd"
168
168
  INSPIRE = "inspire"
169
- HVD = "hvd"
170
169
  SL = "sl"
171
170
  SR = "sr"
172
171
  CLOSED_FORMATS = ("pdf", "doc", "docx", "word", "xls", "excel", "xlsx")
@@ -1,3 +1,10 @@
1
+ from udata.core.access_type.constants import (
2
+ AccessAudienceCondition,
3
+ AccessAudienceType,
4
+ AccessType,
5
+ InspireLimitationCategory,
6
+ )
7
+ from udata.core.access_type.models import AccessAudience
1
8
  from udata.core.spatial.forms import SpatialCoverageField
2
9
  from udata.core.storages import resources
3
10
  from udata.forms import ModelForm, fields, validators
@@ -116,6 +123,8 @@ class CommunityResourceForm(BaseResourceForm):
116
123
 
117
124
 
118
125
  def unmarshal_frequency(form, field):
126
+ if field.data is None:
127
+ return
119
128
  # We don't need to worry about invalid field.data being fed to UpdateFrequency here,
120
129
  # since the API will already have ensured incoming data matches the field definition,
121
130
  # which in our case is an enum of valid UpdateFrequency values.
@@ -139,6 +148,13 @@ def validate_contact_point(form, field):
139
148
  )
140
149
 
141
150
 
151
+ class AccessAudienceForm(ModelForm):
152
+ model_class = AccessAudience
153
+
154
+ role = fields.SelectField(choices=[(e.value, e.value) for e in AccessAudienceType])
155
+ condition = fields.SelectField(choices=[(e.value, e.value) for e in AccessAudienceCondition])
156
+
157
+
142
158
  class DatasetForm(ModelForm):
143
159
  model_class = Dataset
144
160
 
@@ -157,6 +173,19 @@ class DatasetForm(ModelForm):
157
173
  description=_("A short description of the dataset."),
158
174
  )
159
175
  license = fields.ModelSelectField(_("License"), model=License, allow_blank=True)
176
+ access_type = fields.SelectField(
177
+ choices=[(e.value, e.value) for e in AccessType],
178
+ default=AccessType.OPEN,
179
+ validators=[validators.optional()],
180
+ )
181
+ access_audiences = fields.NestedModelList(AccessAudienceForm)
182
+ authorization_request_url = fields.StringField(_("Authorization request URL"))
183
+ access_type_reason_category = fields.SelectField(
184
+ _("Access type reason category"),
185
+ choices=[(e.value, e.label) for e in InspireLimitationCategory],
186
+ validators=[validators.optional()],
187
+ )
188
+ access_type_reason = fields.StringField(_("Access type reason"))
160
189
  frequency = fields.SelectField(
161
190
  _("Update frequency"),
162
191
  choices=list(UpdateFrequency),
@@ -9,6 +9,7 @@ import Levenshtein
9
9
  import requests
10
10
  from blinker import signal
11
11
  from flask import current_app, url_for
12
+ from flask_babel import LazyString
12
13
  from mongoengine import ValidationError as MongoEngineValidationError
13
14
  from mongoengine.fields import DateTimeField
14
15
  from mongoengine.signals import post_save, pre_init, pre_save
@@ -17,7 +18,10 @@ from werkzeug.utils import cached_property
17
18
  from udata.api_fields import field
18
19
  from udata.app import cache
19
20
  from udata.core import storages
21
+ from udata.core.access_type.constants import AccessType
22
+ from udata.core.access_type.models import WithAccessType, check_only_one_condition_per_role
20
23
  from udata.core.activity.models import Auditable
24
+ from udata.core.constants import HVD
21
25
  from udata.core.dataset.preview import TabularAPIPreview
22
26
  from udata.core.linkable import Linkable
23
27
  from udata.core.metrics.helpers import get_stock_metrics
@@ -35,7 +39,6 @@ from .constants import (
35
39
  CLOSED_FORMATS,
36
40
  DEFAULT_LICENSE,
37
41
  DESCRIPTION_SHORT_SIZE_LIMIT,
38
- HVD,
39
42
  INSPIRE,
40
43
  MAX_DISTANCE,
41
44
  PIVOTAL_DATA,
@@ -62,7 +65,7 @@ __all__ = (
62
65
  "ResourceSchema",
63
66
  )
64
67
 
65
- BADGES: dict[str, str] = {
68
+ BADGES: dict[str, LazyString] = {
66
69
  PIVOTAL_DATA: _("Pivotal data"),
67
70
  SPD: _("Reference data public service"),
68
71
  INSPIRE: _("Inspire"),
@@ -530,7 +533,9 @@ class DatasetBadgeMixin(BadgeMixin):
530
533
  __badges__ = BADGES
531
534
 
532
535
 
533
- class Dataset(Auditable, WithMetrics, DatasetBadgeMixin, Owned, Linkable, db.Document):
536
+ class Dataset(
537
+ Auditable, WithMetrics, WithAccessType, DatasetBadgeMixin, Owned, Linkable, db.Document
538
+ ):
534
539
  title = field(db.StringField(required=True))
535
540
  acronym = field(db.StringField(max_length=128))
536
541
  # /!\ do not set directly the slug when creating or updating a dataset
@@ -681,6 +686,10 @@ class Dataset(Auditable, WithMetrics, DatasetBadgeMixin, Owned, Linkable, db.Doc
681
686
 
682
687
  self.quality_cached = self.compute_quality()
683
688
 
689
+ check_only_one_condition_per_role(self.access_audiences)
690
+ if self.access_type and self.access_type != AccessType.OPEN:
691
+ self.license = None
692
+
684
693
  for key, value in self.extras.items():
685
694
  if not key.startswith("custom:"):
686
695
  continue
@@ -780,10 +789,13 @@ class Dataset(Auditable, WithMetrics, DatasetBadgeMixin, Owned, Linkable, db.Doc
780
789
 
781
790
  def compute_last_update(self):
782
791
  """
783
- Use the more recent date we would have on resources (harvest, modified).
792
+ If dataset is harvested and its metadata contains a modified_at date, use it.
793
+ Else, use the more recent date we would have at the resource level (harvest, modified).
784
794
  Default to dataset last_modified if no resource.
785
795
  Resources should be fetched when calling this method.
786
796
  """
797
+ if self.harvest and self.harvest.modified_at:
798
+ return self.harvest.modified_at
787
799
  if self.resources:
788
800
  return max([res.last_modified for res in self.resources])
789
801
  else:
udata/core/dataset/rdf.py CHANGED
@@ -16,6 +16,7 @@ from rdflib.namespace import RDF
16
16
  from rdflib.resource import Resource as RdfResource
17
17
 
18
18
  from udata import i18n, uris
19
+ from udata.core.constants import HVD
19
20
  from udata.core.dataset.models import HarvestDatasetMetadata, HarvestResourceMetadata
20
21
  from udata.core.spatial.models import SpatialCoverage
21
22
  from udata.harvest.exceptions import HarvestSkipException
@@ -330,7 +331,7 @@ def dataset_to_rdf(dataset: Dataset, graph: Graph | None = None) -> RdfResource:
330
331
 
331
332
  # Add DCAT-AP HVD properties if the dataset is tagged hvd.
332
333
  # See https://semiceu.github.io/DCAT-AP/releases/2.2.0-hvd/
333
- is_hvd = current_app.config["HVD_SUPPORT"] and "hvd" in dataset.tags
334
+ is_hvd = current_app.config["HVD_SUPPORT"] and any(b.kind == HVD for b in dataset.badges)
334
335
  if is_hvd:
335
336
  d.add(DCATAP.applicableLegislation, URIRef(HVD_LEGISLATION))
336
337