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
@@ -5,8 +5,8 @@ from rdflib.namespace import FOAF, RDF
5
5
  from rdflib.resource import Resource
6
6
  from werkzeug.datastructures import ImmutableMultiDict
7
7
 
8
+ from udata.core.constants import HVD
8
9
  from udata.core.dataservices.factories import DataserviceFactory, HarvestMetadataFactory
9
- from udata.core.dataset.constants import HVD
10
10
  from udata.core.dataset.factories import DatasetFactory
11
11
  from udata.core.dataset.models import Dataset
12
12
  from udata.core.organization.factories import OrganizationFactory
@@ -14,13 +14,11 @@ from udata.core.site.factories import SiteFactory
14
14
  from udata.core.site.rdf import build_catalog
15
15
  from udata.core.user.factories import UserFactory
16
16
  from udata.rdf import CONTEXT, DCAT, DCT, HYDRA
17
+ from udata.tests.api import PytestOnlyAPITestCase
17
18
  from udata.tests.helpers import assert200, assert404, assert_redirects
18
19
 
19
- pytestmark = pytest.mark.usefixtures("clean_db")
20
20
 
21
-
22
- @pytest.mark.frontend
23
- class CatalogTest:
21
+ class CatalogTest(PytestOnlyAPITestCase):
24
22
  def test_minimal(self, app):
25
23
  site = SiteFactory()
26
24
  home_url = url_for("api.site", _external=True)
@@ -142,8 +140,7 @@ class CatalogTest:
142
140
  assert HYDRA.next not in pagination
143
141
 
144
142
 
145
- @pytest.mark.frontend
146
- class SiteRdfViewsTest:
143
+ class SiteRdfViewsTest(PytestOnlyAPITestCase):
147
144
  def test_expose_jsonld_context(self, client):
148
145
  url = url_for("api.site_jsonld_context")
149
146
  assert url == "/api/1/site/context.jsonld"
@@ -9,7 +9,7 @@ from udata.core.activity.models import Activity, Auditable
9
9
  from udata.core.organization.factories import OrganizationFactory
10
10
  from udata.core.user.factories import UserFactory
11
11
  from udata.models import db
12
- from udata.tests import DBTestMixin, TestCase, WebTestMixin
12
+ from udata.tests.api import APITestCase
13
13
  from udata.tests.helpers import assert_emit, assert_not_emit
14
14
 
15
15
 
@@ -45,7 +45,7 @@ class FakeActivity(Activity):
45
45
  related_to = db.ReferenceField(FakeSubject)
46
46
 
47
47
 
48
- class ActivityTest(WebTestMixin, DBTestMixin, TestCase):
48
+ class ActivityTest(APITestCase):
49
49
  def setUp(self):
50
50
  self.fake = FakeSubject.objects.create(name="fake")
51
51
  self.login()
@@ -120,7 +120,7 @@ class ActivityTest(WebTestMixin, DBTestMixin, TestCase):
120
120
  self.assertEqual(Activity.objects(actor=self.user).count(), 1)
121
121
 
122
122
 
123
- class AuditableTest(WebTestMixin, DBTestMixin, TestCase):
123
+ class AuditableTest(APITestCase):
124
124
  def test_auditable_signals_emission(self):
125
125
  """It should emit appropriate signals on subject fields creation, update and deletion"""
126
126
  with assert_emit(post_save, FakeAuditableSubject.on_create):
@@ -13,12 +13,9 @@ from udata.core.storages import default_image_basename, images
13
13
  from udata.factories import ModelFactory
14
14
  from udata.models import Badge, BadgeMixin, BadgesList, WithMetrics, db
15
15
  from udata.mongo.queryset import DBPaginator, UDataQuerySet
16
+ from udata.tests.api import PytestOnlyDBTestCase
16
17
  from udata.utils import faker
17
18
 
18
- pytestmark = [
19
- pytest.mark.usefixtures("clean_db"),
20
- ]
21
-
22
19
  BIGGEST_IMAGE_SIZE: int = 500
23
20
 
24
21
  BADGES: dict[str, str] = {
@@ -166,7 +163,7 @@ class FakeFactory(ModelFactory):
166
163
  archived = None
167
164
 
168
165
 
169
- class IndexParserTest:
166
+ class IndexParserTest(PytestOnlyDBTestCase):
170
167
  index_parser: RequestParser = Fake.__index_parser__
171
168
  index_parser_args: list[Argument] = Fake.__index_parser__.args
172
169
  index_parser_args_names: set[str] = set([field.name for field in Fake.__index_parser__.args])
@@ -233,7 +230,7 @@ class IndexParserTest:
233
230
  assert set(additional_sorts).issubset(set(choices))
234
231
 
235
232
 
236
- class PatchTest:
233
+ class PatchTest(PytestOnlyDBTestCase):
237
234
  fake_json = {"url": URL_RAISE_ERROR, "description": None}
238
235
 
239
236
  def test_patch_check(self) -> None:
@@ -249,7 +246,7 @@ class PatchTest:
249
246
  patch_and_save(fake, fake_request)
250
247
 
251
248
 
252
- class PatchEmbeddedTest:
249
+ class PatchEmbeddedTest(PytestOnlyDBTestCase):
253
250
  fake_json = {
254
251
  "url": URL_RAISE_ERROR,
255
252
  "description": "desc",
@@ -268,7 +265,7 @@ class PatchEmbeddedTest:
268
265
  patch_and_save(fake, fake_request)
269
266
 
270
267
 
271
- class ApplySortAndFiltersTest:
268
+ class ApplySortAndFiltersTest(PytestOnlyDBTestCase):
272
269
  def test_filterable_field(self, app) -> None:
273
270
  """A filterable field filters the results."""
274
271
  fake1: Fake = FakeFactory(filter_field="test filter")
@@ -334,7 +331,7 @@ class ApplySortAndFiltersTest:
334
331
  assert fake2 not in results
335
332
 
336
333
 
337
- class ApplyPaginationTest:
334
+ class ApplyPaginationTest(PytestOnlyDBTestCase):
338
335
  def test_default_pagination(self, app) -> None:
339
336
  """Results should be returned with default pagination."""
340
337
  [FakeFactory() for _ in range(100)]
udata/tests/test_cors.py CHANGED
@@ -8,8 +8,6 @@ from udata.tests.helpers import assert_status
8
8
 
9
9
 
10
10
  class CorsTest(APITestCase):
11
- modules = []
12
-
13
11
  def test_cors_on_allowed_routes(self):
14
12
  cors_headers = {
15
13
  "Origin": "http://localhost",
@@ -1,8 +1,7 @@
1
- import pytest
1
+ from udata.tests.api import PytestOnlyDBTestCase
2
2
 
3
3
 
4
- @pytest.mark.usefixtures("clean_db")
5
- class ParseUrlCommandTest:
4
+ class ParseUrlCommandTest(PytestOnlyDBTestCase):
6
5
  def test_parse_url(self, cli, requests_mock, caplog) -> None:
7
6
  logs = []
8
7
 
@@ -31,14 +31,11 @@ from udata.models import Dataset, Member
31
31
  from udata.tests.helpers import capture_mails
32
32
  from udata.utils import faker
33
33
 
34
- from . import DBTestMixin, TestCase
35
- from .api import APITestCase
34
+ from .api import APITestCase, DBTestCase
36
35
  from .helpers import assert_emit, assert_not_emit
37
36
 
38
37
 
39
38
  class DiscussionsTest(APITestCase):
40
- modules = []
41
-
42
39
  @pytest.mark.options(SPAM_WORDS=["spam"])
43
40
  def test_new_discussion(self):
44
41
  user = self.login()
@@ -1018,7 +1015,7 @@ class DiscussionsTest(APITestCase):
1018
1015
  self.assert403(response)
1019
1016
 
1020
1017
 
1021
- class DiscussionsNotificationsTest(TestCase, DBTestMixin):
1018
+ class DiscussionsNotificationsTest(DBTestCase):
1022
1019
  def test_notify_user_discussions(self):
1023
1020
  owner = UserFactory()
1024
1021
  dataset = DatasetFactory(owner=owner)
@@ -1091,8 +1088,6 @@ class DiscussionsNotificationsTest(TestCase, DBTestMixin):
1091
1088
 
1092
1089
 
1093
1090
  class DiscussionsMailsTest(APITestCase):
1094
- modules = []
1095
-
1096
1091
  def test_new_discussion_mail(self):
1097
1092
  user = UserFactory()
1098
1093
  owner = UserFactory()
udata/tests/test_mail.py CHANGED
@@ -1,120 +1,156 @@
1
- from contextlib import contextmanager
2
- from smtplib import SMTPRecipientsRefused
3
-
4
1
  import pytest
5
2
 
6
3
  from udata.core.organization.factories import OrganizationFactory
7
4
  from udata.core.user.factories import UserFactory
8
- from udata.mail import mail, mail_sent, send
9
- from udata.tests import DBTestMixin, TestCase
10
- from udata.tests.helpers import assert_emit, assert_not_emit
11
-
12
- SMTPRecipientsRefusedList = ["not-found@udata", "not-found-either@udata"]
13
-
14
-
15
- class FakeSender:
16
- def send(self, msg):
17
- if all(recipient in SMTPRecipientsRefusedList for recipient in msg.recipients):
18
- raise SMTPRecipientsRefused(msg.recipients)
19
- mail_sent.send(msg)
20
-
21
-
22
- class FakeMail:
23
- @contextmanager
24
- def connect(*args, **kw):
25
- yield FakeSender()
26
-
27
-
28
- class MailSendTest(TestCase, DBTestMixin):
29
- def create_app(self):
30
- app = super().create_app()
31
- app.config["SEND_MAIL"] = True
32
- return app
33
-
34
- @pytest.fixture(autouse=True)
35
- def patch_mail(self, mocker):
36
- mocker.patch("udata.mail.mail", FakeMail())
37
-
38
- def test_send_mail(self):
39
- with assert_emit(mail_sent):
40
- send("subject", [UserFactory(email="recipient@udata")], "base")
41
-
42
- def test_send_mail_to_not_found_recipients(self):
43
- with assert_not_emit(mail_sent):
44
- send("subject", [UserFactory(email="not-found@udata")], "base")
45
-
46
- def test_send_mail_to_multiple_recipients_with_some_not_found(self):
47
- recipients = [
48
- UserFactory(email="not-found@udata"),
49
- UserFactory(email="recipient@udata"),
50
- UserFactory(email="not-found-either@udata"),
51
- ]
52
- with assert_emit(mail_sent):
53
- send("subject", recipients, "base")
54
-
55
-
56
- @pytest.mark.usefixtures("clean_db")
57
- @pytest.mark.frontend
58
- class MailCampaignTest:
59
- def test_send_mail_campaign_link_new_member(self, app):
60
- # MTM campaign are only added on web URL (not API ones generated when
61
- # no front-end is configured)
62
- app.config["CDATA_BASE_URL"] = "https://www.data.gouv.fr"
63
-
64
- org = OrganizationFactory()
65
- recipient = UserFactory(email="recipient@udata")
66
-
67
- app.config["SEND_MAIL"] = True
68
- app.config["MAIL_CAMPAIGN"] = ""
69
- with mail.record_messages() as outbox:
70
- send(
71
- "subject",
72
- [recipient],
73
- "new_member",
74
- org=org,
5
+ from udata.i18n import lazy_gettext as _
6
+ from udata.mail import LabelledContent, MailMessage, ParagraphWithLinks, send_mail
7
+ from udata.tests.api import APITestCase
8
+ from udata.tests.helpers import capture_mails
9
+
10
+
11
+ class MailGenerationTest(APITestCase):
12
+ @pytest.mark.options(DEFAULT_LANGUAGE="en")
13
+ def test_simple_mail(self):
14
+ with capture_mails() as mails:
15
+ send_mail(
16
+ UserFactory(email="jane@example.org"),
17
+ MailMessage(_("Unknown"), paragraphs=[_("Some text")]),
18
+ )
19
+
20
+ assert len(mails) == 1
21
+ assert len(mails[0].recipients) == 1
22
+ assert mails[0].recipients[0] == "jane@example.org"
23
+ assert mails[0].subject == "Unknown"
24
+ assert "Some text" in mails[0].body
25
+ assert "Some text" in mails[0].html
26
+
27
+ @pytest.mark.options(DEFAULT_LANGUAGE="en")
28
+ def test_allow_none_in_paragraphs(self):
29
+ with capture_mails() as mails:
30
+ send_mail(
31
+ UserFactory(email="jane@example.org"),
32
+ MailMessage(_("Unknown"), paragraphs=[_("Some text"), None]),
75
33
  )
76
- assert len(outbox) == 1
77
- message = outbox[0]
78
- assert "mtm_campaign" not in message.body
79
- assert "mtm_campaign" not in message.html
80
-
81
- app.config["MAIL_CAMPAIGN"] = "data-gouv-fr"
82
- with mail.record_messages() as outbox:
83
- send(
84
- "subject",
85
- [recipient],
86
- "new_member",
87
- org=org,
34
+
35
+ assert len(mails) == 1
36
+
37
+ @pytest.mark.options(DEFAULT_LANGUAGE="en")
38
+ def test_multiple_recipients(self):
39
+ with capture_mails() as mails:
40
+ send_mail(
41
+ [
42
+ UserFactory(email="jane@example.org"),
43
+ UserFactory(email="john@example.org", prefered_language="fr"),
44
+ ],
45
+ MailMessage(_("Unknown"), paragraphs=[_("Some text")]),
46
+ )
47
+
48
+ assert len(mails) == 2
49
+ assert len(mails[0].recipients) == 1
50
+ assert mails[0].recipients[0] == "jane@example.org"
51
+ assert mails[0].subject == "Unknown"
52
+ assert len(mails[1].recipients) == 1
53
+ assert mails[1].recipients[0] == "john@example.org"
54
+ assert mails[1].subject == "Inconnu"
55
+
56
+ @pytest.mark.options(DEFAULT_LANGUAGE="en")
57
+ def test_use_user_language(self):
58
+ with capture_mails() as mails:
59
+ send_mail(
60
+ UserFactory(email="jane@example.org", prefered_language="fr"),
61
+ MailMessage(_("Unknown"), paragraphs=[_("Some text")]),
88
62
  )
89
- assert len(outbox) == 1
90
- message = outbox[0]
91
- assert "mtm_campaign=data-gouv-fr" in message.body
92
- assert "mtm_campaign=data-gouv-fr" in message.html
93
-
94
- def test_send_mail_campaign_link_badge_added_company(self, app):
95
- # MTM campaign are only added on web URL (not API ones generated when
96
- # no front-end is configured)
97
- app.config["CDATA_BASE_URL"] = "https://www.data.gouv.fr"
98
-
99
- app.config["SEND_MAIL"] = True
100
- org = OrganizationFactory()
101
- org.add_badge("company")
102
- badge = org.badges[0]
103
- badge.created_by = UserFactory()
104
- recipient = UserFactory(email="recipient@udata")
105
-
106
- app.config["MAIL_CAMPAIGN"] = ""
107
- with mail.record_messages() as outbox:
108
- send("subject", [recipient], "badge_added_company", organization=org, badge=badge)
109
- assert len(outbox) == 1
110
- message = outbox[0]
111
- assert "mtm_campaign" not in message.body
112
- assert "mtm_campaign" not in message.html
113
-
114
- app.config["MAIL_CAMPAIGN"] = "data-gouv-fr"
115
- with mail.record_messages() as outbox:
116
- send("subject", [recipient], "badge_added_company", organization=org, badge=badge)
117
- assert len(outbox) == 1
118
- message = outbox[0]
119
- assert "mtm_campaign=data-gouv-fr" in message.body
120
- assert "mtm_campaign=data-gouv-fr" in message.html
63
+
64
+ assert len(mails) == 1
65
+ assert mails[0].subject == "Inconnu"
66
+
67
+ @pytest.mark.options(DEFAULT_LANGUAGE="en")
68
+ def test_use_objects_in_translations(self):
69
+ org = OrganizationFactory(name="My Org")
70
+
71
+ with capture_mails() as mails:
72
+ send_mail(
73
+ UserFactory(email="jane@example.org", prefered_language="fr"),
74
+ MailMessage(
75
+ _("Unknown"),
76
+ paragraphs=[ParagraphWithLinks(_("Some text %(org)s", org=org))],
77
+ ),
78
+ )
79
+
80
+ assert len(mails) == 1
81
+ assert "My Org" in mails[0].body
82
+ assert org.url_for() not in mails[0].body
83
+ assert "My Org" in mails[0].html
84
+ assert "<a" in mails[0].html
85
+ assert org.url_for() in mails[0].html
86
+
87
+ @pytest.mark.options(DEFAULT_LANGUAGE="en")
88
+ def test_labelled_bloc(self):
89
+ with capture_mails() as mails:
90
+ send_mail(
91
+ UserFactory(email="jane@example.org", prefered_language="fr"),
92
+ MailMessage(
93
+ _("Unknown"),
94
+ paragraphs=[LabelledContent(_("Some text:"), "Some content", inline=True)],
95
+ ),
96
+ )
97
+
98
+ assert len(mails) == 1
99
+ assert "<strong>Du texte :</strong> Some content" in mails[0].html
100
+ assert "Du texte : Some content" in mails[0].body
101
+
102
+ @pytest.mark.options(DEFAULT_LANGUAGE="en")
103
+ def test_labelled_bloc_truncation(self):
104
+ with capture_mails() as mails:
105
+ send_mail(
106
+ UserFactory(email="jane@example.org", prefered_language="fr"),
107
+ MailMessage(
108
+ _("Unknown"),
109
+ paragraphs=[
110
+ LabelledContent(
111
+ _("Some text:"),
112
+ """
113
+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.
114
+ """,
115
+ )
116
+ ],
117
+ ),
118
+ )
119
+
120
+ assert len(mails) == 1
121
+ assert "a type specimen book." not in mails[0].html
122
+ assert "a type specimen book." not in mails[0].body
123
+
124
+ @pytest.mark.options(DEFAULT_LANGUAGE="en")
125
+ def test_escape_user_content_in_content(self):
126
+ with capture_mails() as mails:
127
+ send_mail(
128
+ UserFactory(email="jane@example.org", prefered_language="fr"),
129
+ MailMessage(
130
+ _("Unknown"),
131
+ paragraphs=[
132
+ LabelledContent(
133
+ _("Some text:"), "<script>Some content</script>", inline=True
134
+ )
135
+ ],
136
+ ),
137
+ )
138
+
139
+ assert len(mails) == 1
140
+ assert "<script>" not in mails[0].html
141
+
142
+ @pytest.mark.options(DEFAULT_LANGUAGE="en")
143
+ def test_escape_user_content_in_object_label(self):
144
+ org = OrganizationFactory(name="<script>My Org</script>")
145
+
146
+ with capture_mails() as mails:
147
+ send_mail(
148
+ UserFactory(email="jane@example.org", prefered_language="fr"),
149
+ MailMessage(
150
+ _("Unknown"),
151
+ paragraphs=[ParagraphWithLinks(_("Some text %(org)s", org=org))],
152
+ ),
153
+ )
154
+
155
+ assert len(mails) == 1
156
+ assert "<script>" not in mails[0].html