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
@@ -12,12 +12,12 @@ from udata.tests.helpers import capture_mails
12
12
 
13
13
  class SecurityAPITest(PytestOnlyAPITestCase):
14
14
  @pytest.mark.options(CAPTCHETAT_BASE_URL=None)
15
- def test_register(self, api):
15
+ def test_register(self):
16
16
  # We cannot test for mail sending since they are sent with Flask
17
17
  # directly and not with our system but if the sending is working
18
18
  # we test the rendering of the mail.
19
19
 
20
- response = api.post(
20
+ response = self.post(
21
21
  url_for("security.register"),
22
22
  {
23
23
  "first_name": "Jane",
@@ -32,13 +32,13 @@ class SecurityAPITest(PytestOnlyAPITestCase):
32
32
  self.assertStatus(response, 200)
33
33
 
34
34
  @pytest.mark.options(CAPTCHETAT_BASE_URL=None, SECURITY_RETURN_GENERIC_RESPONSES=True)
35
- def test_register_existing(self, api):
35
+ def test_register_existing(self):
36
36
  # We cannot test for mail sending since they are sent with Flask
37
37
  # directly and not with our system but if the sending is working
38
38
  # we test the rendering of the mail.
39
39
 
40
40
  UserFactory(email="jane@example.org", confirmed_at=datetime.now())
41
- response = api.post(
41
+ response = self.post(
42
42
  url_for("security.register"),
43
43
  {
44
44
  "first_name": "Jane",
@@ -53,20 +53,20 @@ class SecurityAPITest(PytestOnlyAPITestCase):
53
53
  self.assertStatus(response, 200)
54
54
 
55
55
  @pytest.mark.options(CAPTCHETAT_BASE_URL=None)
56
- def test_ask_for_reset(self, api):
56
+ def test_ask_for_reset(self):
57
57
  # We cannot test for mail sending since they are sent with Flask
58
58
  # directly and not with our system but if the sending is working
59
59
  # we test the rendering of the mail.
60
60
 
61
61
  UserFactory(email="jane@example.org", confirmed_at=datetime.now())
62
62
 
63
- response = api.post(
63
+ response = self.post(
64
64
  url_for("security.forgot_password"), {"email": "jane@example.org", "submit": True}
65
65
  )
66
66
  self.assertStatus(response, 200)
67
67
 
68
68
  @pytest.mark.options(CAPTCHETAT_BASE_URL=None)
69
- def test_change_notice_mail(self, api):
69
+ def test_change_notice_mail(self):
70
70
  # We cannot test for mail sending since they are sent with Flask
71
71
  # directly and not with our system but if the sending is working
72
72
  # we test the rendering of the mail.
@@ -76,7 +76,7 @@ class SecurityAPITest(PytestOnlyAPITestCase):
76
76
  )
77
77
  self.login(user)
78
78
 
79
- response = api.post(
79
+ response = self.post(
80
80
  url_for("security.change_password"),
81
81
  {
82
82
  "password": "password",
@@ -88,12 +88,12 @@ class SecurityAPITest(PytestOnlyAPITestCase):
88
88
  self.assertStatus(response, 200)
89
89
 
90
90
  @pytest.mark.options(CAPTCHETAT_BASE_URL=None)
91
- def test_change_email_confirmation(self, api):
91
+ def test_change_email_confirmation(self):
92
92
  user = UserFactory(email="jane@example.org", confirmed_at=datetime.now())
93
93
  self.login(user)
94
94
 
95
95
  with capture_mails() as mails:
96
- response = api.post(
96
+ response = self.post(
97
97
  url_for("security.change_email"),
98
98
  {
99
99
  "new_email": "jane2@example.org",
@@ -109,11 +109,11 @@ class SecurityAPITest(PytestOnlyAPITestCase):
109
109
  assert mails[0].subject == _("Confirm your email address")
110
110
 
111
111
  @pytest.mark.options(CAPTCHETAT_BASE_URL=None, SECURITY_RETURN_GENERIC_RESPONSES=True)
112
- def test_reset_password(self, api):
112
+ def test_reset_password(self):
113
113
  user = UserFactory(email="jane@example.org", confirmed_at=datetime.now())
114
114
  token = generate_reset_password_token(user)
115
115
 
116
- response = api.post(
116
+ response = self.post(
117
117
  url_for("security.reset_password", token=token),
118
118
  {
119
119
  "password": "Password123",
@@ -8,16 +8,16 @@ from udata.tests.helpers import assert200
8
8
 
9
9
 
10
10
  class SwaggerBlueprintTest(PytestOnlyAPITestCase):
11
- def test_swagger_resource_type(self, api):
12
- response = api.get(url_for("api.specs"))
11
+ def test_swagger_resource_type(self):
12
+ response = self.get(url_for("api.specs"))
13
13
  assert200(response)
14
14
  swagger = json.loads(response.data)
15
15
  expected = swagger["paths"]["/datasets/{dataset}/resources/"]
16
16
  expected = expected["put"]["responses"]["200"]["schema"]["type"]
17
17
  assert expected == "array"
18
18
 
19
- def test_swagger_specs_validate(self, api):
20
- response = api.get(url_for("api.specs"))
19
+ def test_swagger_specs_validate(self):
20
+ response = self.get(url_for("api.specs"))
21
21
  try:
22
22
  schemas.validate(response.json)
23
23
  except schemas.SchemaValidationError as e:
@@ -9,7 +9,7 @@ from udata.utils import faker
9
9
 
10
10
 
11
11
  class TagsAPITest(PytestOnlyAPITestCase):
12
- def test_suggest_tags_api(self, api):
12
+ def test_suggest_tags_api(self):
13
13
  """It should suggest tags"""
14
14
  for i in range(3):
15
15
  tags = [faker.tag(), faker.tag(), "test", "test-{0}".format(i)]
@@ -18,7 +18,7 @@ class TagsAPITest(PytestOnlyAPITestCase):
18
18
 
19
19
  count_tags()
20
20
 
21
- response = api.get(url_for("api.suggest_tags", q="tes", size=5))
21
+ response = self.get(url_for("api.suggest_tags", q="tes", size=5))
22
22
  assert200(response)
23
23
 
24
24
  assert len(response.json) <= 5
@@ -29,7 +29,7 @@ class TagsAPITest(PytestOnlyAPITestCase):
29
29
  assert "text" in suggestion
30
30
  assert "tes" in suggestion["text"]
31
31
 
32
- def test_suggest_tags_api_with_unicode(self, api):
32
+ def test_suggest_tags_api_with_unicode(self):
33
33
  """It should suggest tags"""
34
34
  for i in range(3):
35
35
  tags = [faker.tag(), faker.tag(), "testé", "testé-{0}".format(i)]
@@ -38,7 +38,7 @@ class TagsAPITest(PytestOnlyAPITestCase):
38
38
 
39
39
  count_tags()
40
40
 
41
- response = api.get(url_for("api.suggest_tags", q="testé", size=5))
41
+ response = self.get(url_for("api.suggest_tags", q="testé", size=5))
42
42
  assert200(response)
43
43
 
44
44
  assert len(response.json) <= 5
@@ -49,7 +49,7 @@ class TagsAPITest(PytestOnlyAPITestCase):
49
49
  assert "text" in suggestion
50
50
  assert "teste" in suggestion["text"]
51
51
 
52
- def test_suggest_tags_api_no_match(self, api):
52
+ def test_suggest_tags_api_no_match(self):
53
53
  """It should not provide tag suggestion if no match"""
54
54
  for i in range(3):
55
55
  tags = ["aaaa", "aaaa-{0}".format(i)]
@@ -58,12 +58,12 @@ class TagsAPITest(PytestOnlyAPITestCase):
58
58
 
59
59
  count_tags()
60
60
 
61
- response = api.get(url_for("api.suggest_tags", q="bbbb", size=5))
61
+ response = self.get(url_for("api.suggest_tags", q="bbbb", size=5))
62
62
  assert200(response)
63
63
  assert len(response.json) == 0
64
64
 
65
- def test_suggest_tags_api_empty(self, api):
65
+ def test_suggest_tags_api_empty(self):
66
66
  """It should not provide tag suggestion if no data"""
67
- response = api.get(url_for("api.suggest_tags", q="bbbb", size=5))
67
+ response = self.get(url_for("api.suggest_tags", q="bbbb", size=5))
68
68
  assert200(response)
69
69
  assert len(response.json) == 0
@@ -175,6 +175,18 @@ class UserAPITest(APITestCase):
175
175
 
176
176
  self.assertEqual(len(response.json["data"]), 2)
177
177
 
178
+ def test_user_api_full_text_search_email(self):
179
+ """It should find users based on last name"""
180
+ self.login(AdminFactory())
181
+
182
+ UserFactory(email="john@example.org")
183
+ UserFactory(email="jane@example.org")
184
+
185
+ response = self.get(url_for("api.users", q="jane"))
186
+ self.assert200(response)
187
+
188
+ self.assertEqual(len(response.json["data"]), 1)
189
+
178
190
  def test_user_api_full_text_search_unicode(self):
179
191
  """It should find user with special characters"""
180
192
  self.login(AdminFactory())
@@ -370,7 +382,7 @@ class UserAPITest(APITestCase):
370
382
  response = self.delete(url_for("api.user", user=user_to_delete))
371
383
  self.assertEqual(list(storages.avatars.list_files()), [])
372
384
  self.assert204(response)
373
- self.assertEquals(len(mails), 1)
385
+ self.assertEqual(len(mails), 1)
374
386
 
375
387
  user_to_delete.reload()
376
388
  response = self.delete(url_for("api.user", user=user_to_delete))
@@ -8,16 +8,16 @@ from udata.tests.helpers import assert200
8
8
 
9
9
 
10
10
  class SwaggerBlueprintTest(PytestOnlyAPITestCase):
11
- def test_swagger_resource_type(self, api):
12
- response = api.get(url_for("apiv2.specs"))
11
+ def test_swagger_resource_type(self):
12
+ response = self.get(url_for("apiv2.specs"))
13
13
  assert200(response)
14
14
  swagger = json.loads(response.data)
15
15
  expected = swagger["paths"]["/datasets/{dataset}/resources/"]
16
16
  expected = expected["get"]["responses"]["200"]["schema"]["$ref"]
17
17
  assert expected == "#/definitions/ResourcePage"
18
18
 
19
- def test_swagger_specs_validate(self, api):
20
- response = api.get(url_for("apiv2.specs"))
19
+ def test_swagger_specs_validate(self):
20
+ response = self.get(url_for("apiv2.specs"))
21
21
  try:
22
22
  schemas.validate(response.json)
23
23
  except schemas.SchemaValidationError as e:
@@ -23,7 +23,7 @@ from udata.core.user.factories import UserFactory
23
23
  from udata.i18n import _
24
24
  from udata.tests.api import APITestCase
25
25
  from udata.tests.api.test_datasets_api import SAMPLE_GEOM
26
- from udata.tests.features.territories import create_geozones_fixtures
26
+ from udata.tests.helpers import create_geozones_fixtures
27
27
 
28
28
 
29
29
  class TopicsListAPITest(APITestCase):
@@ -2,17 +2,16 @@ from udata.tests import PytestOnlyTestCase
2
2
 
3
3
 
4
4
  class CliBaseTest(PytestOnlyTestCase):
5
- def test_cli_help(self, cli):
5
+ def test_cli_help(self):
6
6
  """Should display help without errors"""
7
- cli()
8
- cli("-?")
9
- cli("-h")
10
- cli("--help")
7
+ self.cli("-?")
8
+ self.cli("-h")
9
+ self.cli("--help")
11
10
 
12
- def test_cli_log_and_printing(self, cli):
11
+ def test_cli_log_and_printing(self):
13
12
  """Should properly log and print"""
14
- cli("test log")
13
+ self.cli("test log")
15
14
 
16
- def test_cli_version(self, cli):
15
+ def test_cli_version(self):
17
16
  """Should display version without errors"""
18
- cli("--version")
17
+ self.cli("--version")
@@ -5,22 +5,22 @@ from udata.tests.api import PytestOnlyDBTestCase
5
5
 
6
6
 
7
7
  class DatasetCommandTest(PytestOnlyDBTestCase):
8
- def test_dataset_archive_one(self, cli):
8
+ def test_dataset_archive_one(self):
9
9
  dataset = DatasetFactory()
10
10
 
11
- cli("dataset", "archive-one", str(dataset.id))
11
+ self.cli("dataset", "archive-one", str(dataset.id))
12
12
 
13
13
  dataset.reload()
14
14
  assert dataset.archived is not None
15
15
 
16
- def test_dataset_archive(self, cli):
16
+ def test_dataset_archive(self):
17
17
  datasets = [DatasetFactory() for _ in range(2)]
18
18
 
19
19
  with NamedTemporaryFile(mode="w", encoding="utf8") as temp:
20
20
  temp.write("\n".join((str(d.id) for d in datasets)))
21
21
  temp.flush()
22
22
 
23
- cli("dataset", "archive", temp.name)
23
+ self.cli("dataset", "archive", temp.name)
24
24
 
25
25
  for dataset in datasets:
26
26
  dataset.reload()
@@ -1,4 +1,4 @@
1
- from datetime import datetime, timedelta
1
+ from datetime import date, datetime, timedelta, timezone
2
2
  from uuid import uuid4
3
3
 
4
4
  import pytest
@@ -206,6 +206,18 @@ class DatasetModelTest(PytestOnlyDBTestCase):
206
206
  assert dataset.quality["update_fulfilled_in_time"] is False
207
207
  assert dataset.quality["score"] == Dataset.normalize_score(1)
208
208
 
209
+ def test_quality_frequency_update_with_harvest_timezone_aware(self):
210
+ """Test that update_fulfilled_in_time works with timezone-aware harvest dates."""
211
+ dataset = DatasetFactory(
212
+ description="",
213
+ frequency=UpdateFrequency.DAILY,
214
+ harvest=HarvestDatasetMetadata(
215
+ modified_at=datetime.now(timezone.utc) - timedelta(hours=1),
216
+ ),
217
+ )
218
+ assert dataset.quality["update_frequency"] is True
219
+ assert dataset.quality["update_fulfilled_in_time"] is True
220
+
209
221
  def test_quality_description_length(self):
210
222
  dataset = DatasetFactory(
211
223
  description="a" * (current_app.config.get("QUALITY_DESCRIPTION_LENGTH") - 1)
@@ -326,9 +338,11 @@ class DatasetModelTest(PytestOnlyDBTestCase):
326
338
 
327
339
  assert dataset_without_resources.resources_len == 0
328
340
 
329
- def test_dataset_activities(self, api, mocker):
341
+ def test_dataset_activities(self, app, mocker):
330
342
  # A user must be authenticated for activities to be emitted
331
- user = api.login()
343
+ from flask_login import login_user
344
+
345
+ user = UserFactory()
332
346
 
333
347
  mock_created = mocker.patch.object(UserCreatedDataset, "emit")
334
348
  mock_updated = mocker.patch.object(UserUpdatedDataset, "emit")
@@ -337,35 +351,38 @@ class DatasetModelTest(PytestOnlyDBTestCase):
337
351
  mock_resouce_updated = mocker.patch.object(UserUpdatedResource, "emit")
338
352
  mock_resouce_removed = mocker.patch.object(UserRemovedResourceFromDataset, "emit")
339
353
 
340
- with assert_emit(Dataset.on_create):
341
- dataset = DatasetFactory(owner=user)
342
- mock_created.assert_called()
354
+ with app.test_request_context():
355
+ login_user(user)
343
356
 
344
- with assert_emit(Dataset.on_update):
345
- dataset.title = "new title"
346
- dataset.save()
347
- mock_updated.assert_called()
357
+ with assert_emit(Dataset.on_create):
358
+ dataset = DatasetFactory(owner=user)
359
+ mock_created.assert_called()
348
360
 
349
- with assert_emit(Dataset.on_resource_added):
350
- dataset.add_resource(ResourceFactory())
351
- mock_resource_added.assert_called()
361
+ with assert_emit(Dataset.on_update):
362
+ dataset.title = "new title"
363
+ dataset.save()
364
+ mock_updated.assert_called()
352
365
 
353
- dataset.reload()
366
+ with assert_emit(Dataset.on_resource_added):
367
+ dataset.add_resource(ResourceFactory())
368
+ mock_resource_added.assert_called()
354
369
 
355
- with assert_emit(Dataset.on_resource_updated):
356
- resource = dataset.resources[0]
357
- resource.description = "New description"
358
- dataset.update_resource(resource)
359
- mock_resouce_updated.assert_called()
370
+ dataset.reload()
360
371
 
361
- with assert_emit(Dataset.on_resource_removed):
362
- dataset.remove_resource(dataset.resources[-1])
363
- mock_resouce_removed.assert_called()
372
+ with assert_emit(Dataset.on_resource_updated):
373
+ resource = dataset.resources[0]
374
+ resource.description = "New description"
375
+ dataset.update_resource(resource)
376
+ mock_resouce_updated.assert_called()
364
377
 
365
- with assert_emit(Dataset.on_delete):
366
- dataset.deleted = datetime.utcnow()
367
- dataset.save()
368
- mock_deleted.assert_called()
378
+ with assert_emit(Dataset.on_resource_removed):
379
+ dataset.remove_resource(dataset.resources[-1])
380
+ mock_resouce_removed.assert_called()
381
+
382
+ with assert_emit(Dataset.on_delete):
383
+ dataset.deleted = datetime.utcnow()
384
+ dataset.save()
385
+ mock_deleted.assert_called()
369
386
 
370
387
  def test_dataset_metrics(self):
371
388
  # We need to init metrics module
@@ -867,3 +884,26 @@ class HarvestMetadataTest(PytestOnlyDBTestCase):
867
884
  resource.validate()
868
885
 
869
886
  assert resource.last_modified == resource.extras["analysis:last-modified-at"]
887
+
888
+ def test_quality_cached_next_update_with_date_last_update(self):
889
+ """Test that quality_cached with date (not datetime) last_update can be saved to MongoDB.
890
+
891
+ This reproduces a production bug where last_update could be a datetime.date instead
892
+ of datetime.datetime, causing next_update to also be a datetime.date, which BSON
893
+ cannot encode (it only supports datetime.datetime).
894
+
895
+ See error: bson.errors.InvalidDocument: cannot encode object: datetime.date(...), of type: <class 'datetime.date'>
896
+ """
897
+ dataset: Dataset = DatasetFactory(
898
+ frequency=UpdateFrequency.QUARTERLY,
899
+ )
900
+ # Set harvest metadata with modified_at as a date instead of datetime
901
+ # This simulates data coming from harvesting where dates might not be properly typed
902
+ dataset.harvest = HarvestDatasetMetadata(
903
+ created_at=datetime(2019, 1, 1),
904
+ modified_at=date(2019, 6, 7), # Using date instead of datetime
905
+ )
906
+ # Also set last_update as date to fully simulate the production scenario
907
+ dataset.last_update = date(2019, 6, 7)
908
+
909
+ dataset.save()
@@ -643,10 +643,10 @@ class RdfToDatasetTest(PytestOnlyDBTestCase):
643
643
 
644
644
  assert len(dataset.contact_points) == 1
645
645
  assert dataset.contact_points[0].role == "contact"
646
- assert dataset.contact_points[0].name == "foo"
646
+ assert dataset.contact_points[0].name == "foo (bar)"
647
647
  assert dataset.contact_points[0].email == "foo@example.com"
648
648
 
649
- def test_contact_point_organization_member_foaf(self):
649
+ def test_contact_point_organization_member_foaf_both_mails(self):
650
650
  g = Graph()
651
651
  node = URIRef("https://test.org/dataset")
652
652
  g.set((node, RDF.type, DCAT.Dataset))
@@ -673,10 +673,10 @@ class RdfToDatasetTest(PytestOnlyDBTestCase):
673
673
 
674
674
  assert len(dataset.contact_points) == 1
675
675
  assert dataset.contact_points[0].role == "creator"
676
- assert dataset.contact_points[0].name == "foo"
676
+ assert dataset.contact_points[0].name == "foo (bar)"
677
677
  assert dataset.contact_points[0].email == "foo@example.com"
678
678
 
679
- def test_contact_point_organization_member_foaf_no_mail(self):
679
+ def test_contact_point_organization_member_foaf_no_org_mail(self):
680
680
  g = Graph()
681
681
  node = URIRef("https://test.org/dataset")
682
682
  g.set((node, RDF.type, DCAT.Dataset))
@@ -703,9 +703,39 @@ class RdfToDatasetTest(PytestOnlyDBTestCase):
703
703
 
704
704
  assert len(dataset.contact_points) == 1
705
705
  assert dataset.contact_points[0].role == "creator"
706
- assert dataset.contact_points[0].name == "foo"
706
+ assert dataset.contact_points[0].name == "foo (bar)"
707
707
  assert dataset.contact_points[0].email == "foo@example.com"
708
708
 
709
+ def test_contact_point_organization_member_foaf_no_agent_mail(self):
710
+ g = Graph()
711
+ node = URIRef("https://test.org/dataset")
712
+ g.set((node, RDF.type, DCAT.Dataset))
713
+ g.set((node, DCT.identifier, Literal(faker.uuid4())))
714
+ g.set((node, DCT.title, Literal(faker.sentence())))
715
+
716
+ org = BNode()
717
+ g.add((org, RDF.type, FOAF.Organization))
718
+ g.add((org, FOAF.name, Literal("bar")))
719
+ g.add((org, FOAF.mbox, Literal("bar@example.com")))
720
+ contact = BNode()
721
+ g.add((contact, RDF.type, FOAF.Person))
722
+ g.add((contact, FOAF.name, Literal("foo")))
723
+ # no agent email
724
+ g.add((contact, ORG.memberOf, org))
725
+ g.add((node, DCT.creator, contact))
726
+
727
+ # Dataset needs an owner/organization for contact_points_from_rdf() to work
728
+ d = DatasetFactory.build()
729
+ d.organization = OrganizationFactory(name="organization")
730
+
731
+ dataset = dataset_from_rdf(g, d)
732
+ dataset.validate()
733
+
734
+ assert len(dataset.contact_points) == 1
735
+ assert dataset.contact_points[0].role == "creator"
736
+ assert dataset.contact_points[0].name == "foo (bar)"
737
+ assert dataset.contact_points[0].email == "bar@example.com"
738
+
709
739
  def test_theme_and_tags(self):
710
740
  node = BNode()
711
741
  g = Graph()
@@ -1364,6 +1394,70 @@ class DatasetRdfViewsTest(PytestOnlyAPITestCase):
1364
1394
  assert200(response)
1365
1395
  assert response.content_type == mime
1366
1396
 
1397
+ @pytest.mark.parametrize(
1398
+ "fmt,mime",
1399
+ [
1400
+ ("n3", "text/n3"),
1401
+ ("nt", "application/n-triples"),
1402
+ ("ttl", "application/x-turtle"),
1403
+ ("xml", "application/rdf+xml"),
1404
+ ("rdf", "application/rdf+xml"),
1405
+ ("owl", "application/rdf+xml"),
1406
+ ("trig", "application/trig"),
1407
+ ],
1408
+ )
1409
+ def test_dont_fail_with_invalid_uri(self, client, fmt, mime):
1410
+ """Invalid URIs (with spaces or curly brackets) shouldn't make rdf export fail in any format"""
1411
+ invalid_uri_with_quote = 'https://test.org/dataset_with"quote"'
1412
+ invalid_uri_with_curly_bracket = 'http://opendata-sig.saintdenis.re/datasets/identifiant.kml?outSR={"latestWkid":2975,"wkid":2975}'
1413
+ invalid_uri_with_space = "https://catalogue.opendata-ligair.fr/geonetwork/srv/60678572-36e5-4e78-9af3-48f726670dfd fr-modelisation-sirane-vacarm_no2"
1414
+ dataset = DatasetFactory(
1415
+ resources=[
1416
+ ResourceFactory(url=invalid_uri_with_quote),
1417
+ ResourceFactory(url=invalid_uri_with_curly_bracket),
1418
+ ],
1419
+ harvest=HarvestDatasetMetadata(uri=invalid_uri_with_space),
1420
+ )
1421
+
1422
+ url = url_for("api.dataset_rdf_format", dataset=dataset, _format=fmt)
1423
+ response = client.get(url, headers={"Accept": mime})
1424
+ assert200(response)
1425
+
1426
+ @pytest.mark.parametrize(
1427
+ "fmt,mime",
1428
+ [
1429
+ ("n3", "text/n3"),
1430
+ ("nt", "application/n-triples"),
1431
+ ("ttl", "application/x-turtle"),
1432
+ ("trig", "application/trig"),
1433
+ ],
1434
+ )
1435
+ def test_invalid_uri_escape_in_n3_turtle_format(self, client, fmt, mime):
1436
+ """Invalid URIs (with spaces or curly brackets) should be escaped in N3/turtle formats"""
1437
+ invalid_uri_with_quote = 'https://test.org/dataset_with"quote"'
1438
+ invalid_uri_with_curly_bracket = 'http://opendata-sig.saintdenis.re/datasets/identifiant.kml?outSR={"latestWkid":2975,"wkid":2975}'
1439
+ invalid_uri_with_space = "https://catalogue.opendata-ligair.fr/geonetwork/srv/60678572-36e5-4e78-9af3-48f726670dfd fr-modelisation-sirane-vacarm_no2"
1440
+ dataset = DatasetFactory(
1441
+ resources=[
1442
+ ResourceFactory(url=invalid_uri_with_quote),
1443
+ ResourceFactory(url=invalid_uri_with_curly_bracket),
1444
+ ],
1445
+ harvest=HarvestDatasetMetadata(uri=invalid_uri_with_space),
1446
+ )
1447
+
1448
+ url = url_for("api.dataset_rdf_format", dataset=dataset, _format=fmt)
1449
+ response = client.get(url, headers={"Accept": mime})
1450
+ assert200(response)
1451
+ assert "https://test.org/dataset_with%22quote%22" in response.text
1452
+ assert (
1453
+ "http://opendata-sig.saintdenis.re/datasets/identifiant.kml?outSR=%7B%22latestWkid%22:2975,%22wkid%22:2975%7D"
1454
+ in response.text
1455
+ )
1456
+ assert (
1457
+ "https://catalogue.opendata-ligair.fr/geonetwork/srv/60678572-36e5-4e78-9af3-48f726670dfd%20fr-modelisation-sirane-vacarm_no2"
1458
+ in response.text
1459
+ )
1460
+
1367
1461
 
1368
1462
  class DatasetFromRdfUtilsTest(PytestOnlyTestCase):
1369
1463
  def test_licenses_from_rdf(self):
@@ -10,7 +10,6 @@ DUMMY_EXTRAS = {
10
10
  MAX_SIZE = 50000
11
11
 
12
12
 
13
- @pytest.mark.options(PLUGINS=["tabular"])
14
13
  class ResourcePreviewTest(PytestOnlyAPITestCase):
15
14
  def expected_url(self, rid):
16
15
  return "http://preview.me/resources/{0}".format(rid)
@@ -1,7 +1,7 @@
1
1
  from flask import current_app, url_for
2
2
  from flask_security.utils import hash_data
3
3
 
4
- from udata.core.user.factories import AdminFactory
4
+ from udata.core.user.factories import AdminFactory, UserFactory
5
5
  from udata.tests.api import APITestCase
6
6
 
7
7
 
@@ -22,3 +22,26 @@ class AuthTest(APITestCase):
22
22
 
23
23
  user.reload()
24
24
  assert user.email == new_email
25
+
26
+ def test_change_mail_already_taken(self):
27
+ """Should not allow changing email to one already taken by another user"""
28
+ user = self.login(AdminFactory())
29
+ original_email = user.email
30
+
31
+ # Create another user with the target email
32
+ existing_user = UserFactory(email="taken@example.com")
33
+ new_email = existing_user.email
34
+
35
+ security = current_app.extensions["security"]
36
+
37
+ data = [str(user.fs_uniquifier), hash_data(user.email), new_email]
38
+ token = security.confirm_serializer.dumps(data)
39
+ confirmation_link = url_for("security.confirm_change_email", token=token)
40
+
41
+ resp = self.get(confirmation_link)
42
+ assert resp.status_code == 302
43
+ assert "change_email_already_taken" in resp.location
44
+
45
+ # Email should not have changed
46
+ user.reload()
47
+ assert user.email == original_email
@@ -269,7 +269,6 @@ class CsvTest(APITestCase):
269
269
 
270
270
  self.assert200(response)
271
271
  self.assertEqual(response.mimetype, "text/csv")
272
- self.assertEqual(response.charset, "utf-8")
273
272
 
274
273
  csvfile = StringIO(response.data.decode("utf8"))
275
274
  reader = csv.get_reader(csvfile)
@@ -327,7 +326,6 @@ class CsvTest(APITestCase):
327
326
 
328
327
  self.assert200(response)
329
328
  self.assertEqual(response.mimetype, "text/csv")
330
- self.assertEqual(response.charset, "utf-8")
331
329
 
332
330
  csvfile = StringIO(response.data.decode("utf8"))
333
331
  reader = csv.get_reader(csvfile)
@@ -349,7 +347,6 @@ class CsvTest(APITestCase):
349
347
 
350
348
  self.assert200(response)
351
349
  self.assertEqual(response.mimetype, "text/csv")
352
- self.assertEqual(response.charset, "utf-8")
353
350
 
354
351
  csvfile = StringIO(response.data.decode("utf8"))
355
352
  reader = csv.get_reader(csvfile)