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
@@ -0,0 +1,21 @@
1
+ from udata.core.dataset.models import Dataset
2
+ from udata.core.reuse.models import Reuse
3
+ from udata.i18n import lazy_gettext as _
4
+ from udata.mail import LabelledContent, MailCTA, MailMessage, ParagraphWithLinks
5
+
6
+
7
+ def new_reuse(reuse: Reuse, dataset: Dataset) -> MailMessage:
8
+ return MailMessage(
9
+ subject=_("New reuse on your dataset"),
10
+ paragraphs=[
11
+ ParagraphWithLinks(
12
+ _(
13
+ "A new reuse has been published by %(user_or_org)s on your dataset %(dataset)s",
14
+ user_or_org=reuse.organization or reuse.owner,
15
+ dataset=dataset,
16
+ )
17
+ ),
18
+ LabelledContent(_("Reuse title:"), str(reuse.title)),
19
+ MailCTA(_("View the reuse"), reuse.url_for()),
20
+ ],
21
+ )
@@ -1,5 +1,6 @@
1
1
  from blinker import Signal
2
2
  from flask import url_for
3
+ from flask_babel import LazyString
3
4
  from mongoengine.signals import post_save, pre_save
4
5
  from werkzeug.utils import cached_property
5
6
 
@@ -22,7 +23,7 @@ from .constants import IMAGE_MAX_SIZE, IMAGE_SIZES, REUSE_TOPICS, REUSE_TYPES
22
23
 
23
24
  __all__ = ("Reuse",)
24
25
 
25
- BADGES: dict[str, str] = {}
26
+ BADGES: dict[str, LazyString] = {}
26
27
 
27
28
 
28
29
  class ReuseQuerySet(OwnedQuerySet):
@@ -115,6 +116,14 @@ class Reuse(db.Datetimed, Auditable, WithMetrics, ReuseBadgeMixin, Linkable, Own
115
116
  "key": "dataset",
116
117
  },
117
118
  )
119
+ dataservices = field(
120
+ db.ListField(
121
+ field(db.ReferenceField("Dataservice", reverse_delete_rule=db.PULL)),
122
+ ),
123
+ filterable={
124
+ "key": "dataservice",
125
+ },
126
+ )
118
127
  tags = field(
119
128
  db.TagListField(),
120
129
  filterable={
@@ -40,7 +40,7 @@ class ReuseSearch(ModelSearchAdapter):
40
40
 
41
41
  @classmethod
42
42
  def is_indexable(cls, reuse: Reuse) -> bool:
43
- return reuse.deleted is None and len(reuse.datasets) > 0 and not reuse.private
43
+ return reuse.is_visible
44
44
 
45
45
  @classmethod
46
46
  def mongo_search(cls, args):
udata/core/reuse/tasks.py CHANGED
@@ -1,10 +1,9 @@
1
- from udata import mail
2
1
  from udata.core import storages
3
2
  from udata.core.topic.models import TopicElement
4
- from udata.i18n import lazy_gettext as _
5
3
  from udata.models import Activity, Discussion, Follow, Transfer
6
4
  from udata.tasks import get_logger, job, task
7
5
 
6
+ from . import mails
8
7
  from .models import Reuse
9
8
 
10
9
  log = get_logger(__name__)
@@ -45,4 +44,4 @@ def notify_new_reuse(reuse_id: int) -> None:
45
44
  else:
46
45
  recipients = None
47
46
  if recipients:
48
- mail.send(_("New reuse"), recipients, "new_reuse", reuse=reuse, dataset=dataset)
47
+ mails.new_reuse(reuse, dataset).send(recipients)
udata/core/site/models.py CHANGED
@@ -8,6 +8,7 @@ from udata.core.metrics.helpers import get_metrics_for_model, get_stock_metrics
8
8
  from udata.core.organization.models import Organization
9
9
  from udata.core.reuse.models import Reuse
10
10
  from udata.models import WithMetrics, db
11
+ from udata.utils import get_udata_version
11
12
 
12
13
  __all__ = ("Site", "SiteSettings")
13
14
 
@@ -66,12 +67,7 @@ class Site(WithMetrics, db.Document):
66
67
 
67
68
  @field(description="The current version of udata")
68
69
  def version(self):
69
- try:
70
- from importlib.metadata import version
71
-
72
- return version("udata")
73
- except Exception:
74
- return None
70
+ return get_udata_version()
75
71
 
76
72
  def count_users(self):
77
73
  from udata.models import User
@@ -1,3 +1,4 @@
1
+ import pytest
1
2
  from flask import url_for
2
3
 
3
4
  from udata.core.dataset.factories import DatasetFactory
@@ -13,15 +14,12 @@ from udata.core.spatial.tasks import compute_geozones_metrics
13
14
  from udata.tests.api import APITestCase
14
15
  from udata.tests.api.test_datasets_api import SAMPLE_GEOM
15
16
  from udata.tests.features.territories import (
16
- TerritoriesSettings,
17
17
  create_geozones_fixtures,
18
18
  )
19
19
  from udata.utils import faker
20
20
 
21
21
 
22
22
  class SpatialApiTest(APITestCase):
23
- modules = []
24
-
25
23
  def test_zones_api_one(self):
26
24
  zone = GeoZoneFactory()
27
25
 
@@ -65,7 +63,7 @@ class SpatialApiTest(APITestCase):
65
63
  for i in range(4):
66
64
  GeoZoneFactory(name="name-test-{0}".format(i) if i % 2 else faker.word())
67
65
 
68
- response = self.get(url_for("api.suggest_zones"), qs={"q": "name-test", "size": "5"})
66
+ response = self.get(url_for("api.suggest_zones", q="name-test", size=5))
69
67
  self.assert200(response)
70
68
 
71
69
  self.assertEqual(len(response.json), 2)
@@ -84,7 +82,7 @@ class SpatialApiTest(APITestCase):
84
82
  for _ in range(2):
85
83
  GeoZoneFactory()
86
84
 
87
- response = self.get(url_for("api.suggest_zones"), qs={"q": zone.id})
85
+ response = self.get(url_for("api.suggest_zones", q=zone.id))
88
86
  self.assert200(response)
89
87
 
90
88
  self.assertEqual(response.json[0]["id"], zone["id"])
@@ -96,7 +94,7 @@ class SpatialApiTest(APITestCase):
96
94
  country_zone = GeoZoneFactory(name="name-test-country", level=country_level.id)
97
95
  region_zone = GeoZoneFactory(name="name-test-region", level=region_level.id)
98
96
 
99
- response = self.get(url_for("api.suggest_zones"), qs={"q": "name-test", "size": "5"})
97
+ response = self.get(url_for("api.suggest_zones", q="name-test", size=5))
100
98
  self.assert200(response)
101
99
 
102
100
  self.assertEqual(len(response.json), 2)
@@ -108,7 +106,7 @@ class SpatialApiTest(APITestCase):
108
106
  for i in range(4):
109
107
  GeoZoneFactory(code="code-test-{0}".format(i) if i % 2 else faker.word())
110
108
 
111
- response = self.get(url_for("api.suggest_zones"), qs={"q": "code-test", "size": "5"})
109
+ response = self.get(url_for("api.suggest_zones", q="code-test", size=5))
112
110
  self.assert200(response)
113
111
 
114
112
  self.assertEqual(len(response.json), 2)
@@ -126,7 +124,7 @@ class SpatialApiTest(APITestCase):
126
124
  for i in range(3):
127
125
  GeoZoneFactory(name=5 * "{0}".format(i), code=3 * "{0}".format(i))
128
126
 
129
- response = self.get(url_for("api.suggest_zones"), qs={"q": "xxxxxx", "size": "5"})
127
+ response = self.get(url_for("api.suggest_zones", q="xxxxxx", size=5))
130
128
  self.assert200(response)
131
129
  self.assertEqual(len(response.json), 0)
132
130
 
@@ -135,7 +133,7 @@ class SpatialApiTest(APITestCase):
135
133
  for i in range(4):
136
134
  GeoZoneFactory(name="name-testé-{0}".format(i) if i % 2 else faker.word())
137
135
 
138
- response = self.get(url_for("api.suggest_zones"), qs={"q": "name-testé", "size": "5"})
136
+ response = self.get(url_for("api.suggest_zones", q="name-testé", size=5))
139
137
  self.assert200(response)
140
138
 
141
139
  self.assertEqual(len(response.json), 2)
@@ -150,7 +148,7 @@ class SpatialApiTest(APITestCase):
150
148
 
151
149
  def test_suggest_zones_empty(self):
152
150
  """It should not provide zones suggestion if no data is present"""
153
- response = self.get(url_for("api.suggest_zones"), qs={"q": "xxxxxx", "size": "5"})
151
+ response = self.get(url_for("api.suggest_zones", q="xxxxxx", size=5))
154
152
  self.assert200(response)
155
153
  self.assertEqual(len(response.json), 0)
156
154
 
@@ -194,7 +192,7 @@ class SpatialApiTest(APITestCase):
194
192
  organization=organization, spatial=SpatialCoverageFactory(zones=[paca.id])
195
193
  )
196
194
 
197
- response = self.get(url_for("api.zone_datasets", id=paca.id), qs={"size": 2})
195
+ response = self.get(url_for("api.zone_datasets", id=paca.id, size=2))
198
196
  self.assert200(response)
199
197
  self.assertEqual(len(response.json), 2)
200
198
 
@@ -206,7 +204,7 @@ class SpatialApiTest(APITestCase):
206
204
  organization=organization, spatial=SpatialCoverageFactory(zones=[paca.id])
207
205
  )
208
206
 
209
- response = self.get(url_for("api.zone_datasets", id=paca.id), qs={"dynamic": 1})
207
+ response = self.get(url_for("api.zone_datasets", id=paca.id, dynamic=1))
210
208
  self.assert200(response)
211
209
  # No dynamic datasets given that the setting is deactivated by default.
212
210
  self.assertEqual(len(response.json), 3)
@@ -219,7 +217,7 @@ class SpatialApiTest(APITestCase):
219
217
  organization=organization, spatial=SpatialCoverageFactory(zones=[paca.id])
220
218
  )
221
219
 
222
- response = self.get(url_for("api.zone_datasets", id=paca.id), qs={"dynamic": 1, "size": 2})
220
+ response = self.get(url_for("api.zone_datasets", id=paca.id, dynamic=1, size=2))
223
221
  self.assert200(response)
224
222
  # No dynamic datasets given that the setting is deactivated by default.
225
223
  self.assertEqual(len(response.json), 2)
@@ -260,10 +258,11 @@ class SpatialApiTest(APITestCase):
260
258
  self.assertEqual(response.json["features"][1]["properties"]["datasets"], 3)
261
259
 
262
260
 
261
+ @pytest.mark.options(
262
+ ACTIVATE_TERRITORIES=True,
263
+ HANDLED_LEVELS=("fr:commune", "fr:departement", "fr:region", "country"),
264
+ )
263
265
  class SpatialTerritoriesApiTest(APITestCase):
264
- modules = []
265
- settings = TerritoriesSettings
266
-
267
266
  def test_zone_datasets_with_dynamic_and_setting(self):
268
267
  paca, bdr, arles = create_geozones_fixtures()
269
268
  organization = OrganizationFactory()
@@ -272,7 +271,7 @@ class SpatialTerritoriesApiTest(APITestCase):
272
271
  organization=organization, spatial=SpatialCoverageFactory(zones=[paca.id])
273
272
  )
274
273
 
275
- response = self.get(url_for("api.zone_datasets", id=paca.id), qs={"dynamic": 1})
274
+ response = self.get(url_for("api.zone_datasets", id=paca.id, dynamic=1))
276
275
  self.assert200(response)
277
276
  # No dynamic datasets given that they are added by udata-front extension.
278
277
  self.assertEqual(len(response.json), 3)
@@ -285,15 +284,13 @@ class SpatialTerritoriesApiTest(APITestCase):
285
284
  organization=organization, spatial=SpatialCoverageFactory(zones=[paca.id])
286
285
  )
287
286
 
288
- response = self.get(url_for("api.zone_datasets", id=paca.id), qs={"dynamic": 1, "size": 2})
287
+ response = self.get(url_for("api.zone_datasets", id=paca.id, dynamic=1, size=2))
289
288
  self.assert200(response)
290
289
  # No dynamic datasets given that they are added by udata-front extension.
291
290
  self.assertEqual(len(response.json), 2)
292
291
 
293
292
 
294
293
  class DatasetsSpatialAPITest(APITestCase):
295
- modules = []
296
-
297
294
  def test_create_spatial_zones(self):
298
295
  paca, _, _ = create_geozones_fixtures()
299
296
  granularity = spatial_granularities[0][0]
@@ -1,6 +1,6 @@
1
1
  from datetime import timedelta
2
2
 
3
- from udata.tests import DBTestMixin, TestCase
3
+ from udata.tests.api import DBTestCase
4
4
 
5
5
  from ..factories import GeoZoneFactory
6
6
  from ..models import GeoZone, SpatialCoverage
@@ -8,7 +8,7 @@ from ..models import GeoZone, SpatialCoverage
8
8
  A_YEAR = timedelta(days=365)
9
9
 
10
10
 
11
- class SpacialCoverageTest(DBTestMixin, TestCase):
11
+ class SpacialCoverageTest(DBTestCase):
12
12
  def test_top_label_empty(self):
13
13
  coverage = SpatialCoverage()
14
14
  self.assertIsNone(coverage.top_label)
@@ -19,7 +19,7 @@ class SpacialCoverageTest(DBTestMixin, TestCase):
19
19
  self.assertEqual(coverage.top_label, "name")
20
20
 
21
21
 
22
- class SpatialTemporalResolutionTest(DBTestMixin, TestCase):
22
+ class SpatialTemporalResolutionTest(DBTestCase):
23
23
  def test_resolve_id_only(self):
24
24
  zone = GeoZoneFactory()
25
25
  for i in range(3):
@@ -0,0 +1,54 @@
1
+ from flask import current_app
2
+
3
+ from udata.i18n import lazy_gettext as _
4
+ from udata.mail import MailCTA, MailMessage
5
+ from udata.uris import homepage_url
6
+
7
+
8
+ def account_deletion() -> MailMessage:
9
+ return MailMessage(
10
+ subject=_("Account deletion"),
11
+ paragraphs=[_("Your account has now been deleted")],
12
+ )
13
+
14
+
15
+ def inactive_account_deleted() -> MailMessage:
16
+ return MailMessage(
17
+ subject=_(
18
+ "Deletion of your inactive %(site)s account", site=current_app.config["SITE_TITLE"]
19
+ ),
20
+ paragraphs=[
21
+ _(
22
+ "Your account on %(site)s has been deleted due to inactivity",
23
+ site=current_app.config["SITE_TITLE"],
24
+ )
25
+ ],
26
+ )
27
+
28
+
29
+ def inactive_user(user) -> MailMessage:
30
+ config = current_app.config
31
+
32
+ return MailMessage(
33
+ subject=_("Inactivity of your {site} account").format(site=config["SITE_TITLE"]),
34
+ paragraphs=[
35
+ _(
36
+ "We have noticed that your account associated to (%(user_email)s) has been inactive for %(inactivity_years)d years or more on %(site)s, the open platform for public data.",
37
+ user_email=user.email,
38
+ inactivity_years=config["YEARS_OF_INACTIVITY_BEFORE_DELETION"],
39
+ site=config["SITE_TITLE"],
40
+ ),
41
+ MailCTA(
42
+ label=_("If you want to keep your account, please log in with your account."),
43
+ link=homepage_url(),
44
+ ),
45
+ _(
46
+ "Without logging in, your account will be deleted within %(notify_delay)d days.",
47
+ notify_delay=config["DAYS_BEFORE_ACCOUNT_INACTIVITY_NOTIFY_DELAY"],
48
+ ),
49
+ _(
50
+ "This account is not tied to your other administration accounts and you can always re-create an account on the %(site)s platform if necessary.",
51
+ site=config["SITE_TITLE"],
52
+ ),
53
+ ],
54
+ )
udata/core/user/models.py CHANGED
@@ -12,17 +12,16 @@ from flask_security import MongoEngineUserDatastore, RoleMixin, UserMixin
12
12
  from mongoengine.signals import post_save, pre_save
13
13
  from werkzeug.utils import cached_property
14
14
 
15
- from udata import mail
16
15
  from udata.api_fields import field
17
16
  from udata.core import storages
18
17
  from udata.core.discussions.models import Discussion
19
18
  from udata.core.linkable import Linkable
20
19
  from udata.core.storages import avatars, default_image_basename
21
20
  from udata.frontend.markdown import mdstrip
22
- from udata.i18n import lazy_gettext as _
23
21
  from udata.models import Follow, WithMetrics, db
24
22
  from udata.uris import cdata_url
25
23
 
24
+ from . import mails
26
25
  from .constants import AVATAR_SIZES
27
26
 
28
27
  __all__ = ("User", "Role", "datastore")
@@ -295,7 +294,7 @@ class User(WithMetrics, UserMixin, Linkable, db.Document):
295
294
  ContactPoint.objects(owner=self).delete()
296
295
 
297
296
  if notify:
298
- mail.send(_("Account deletion"), copied_user, "account_deleted")
297
+ mails.account_deletion().send(copied_user)
299
298
 
300
299
  def count_datasets(self):
301
300
  from udata.models import Dataset
udata/core/user/tasks.py CHANGED
@@ -4,21 +4,14 @@ from datetime import datetime, timedelta
4
4
 
5
5
  from flask import current_app
6
6
 
7
- from udata import mail
8
- from udata.i18n import lazy_gettext as _
9
- from udata.tasks import job, task
7
+ from udata.tasks import job
10
8
 
11
- from .models import User, datastore
9
+ from . import mails
10
+ from .models import User
12
11
 
13
12
  log = logging.getLogger(__name__)
14
13
 
15
14
 
16
- @task(route="high.mail")
17
- def send_test_mail(email):
18
- user = datastore.find_user(email=email)
19
- mail.send(_("Test mail"), user, "test")
20
-
21
-
22
15
  @job("notify-inactive-users")
23
16
  def notify_inactive_users(self):
24
17
  if not current_app.config["YEARS_OF_INACTIVITY_BEFORE_DELETION"]:
@@ -41,12 +34,9 @@ def notify_inactive_users(self):
41
34
  if i >= current_app.config["MAX_NUMBER_OF_USER_INACTIVITY_NOTIFICATIONS"]:
42
35
  logging.warning("MAX_NUMBER_OF_USER_INACTIVITY_NOTIFICATIONS reached, stopping here.")
43
36
  return
44
- mail.send(
45
- _("Inactivity of your {site} account").format(site=current_app.config["SITE_TITLE"]),
46
- user,
47
- "account_inactivity",
48
- user=user,
49
- )
37
+
38
+ mails.inactive_user(user).send(user)
39
+
50
40
  logging.debug(f"Notified {user.email} of account inactivity")
51
41
  user.inactive_deletion_notified_at = datetime.utcnow()
52
42
  user.save()
@@ -84,11 +74,6 @@ def delete_inactive_users(self):
84
74
  copied_user = copy(user)
85
75
  user.mark_as_deleted(notify=False, delete_comments=False)
86
76
  logging.warning(f"Deleted user {copied_user.email} due to account inactivity")
87
- mail.send(
88
- _("Deletion of your inactive {site} account").format(
89
- site=current_app.config["SITE_TITLE"]
90
- ),
91
- copied_user,
92
- "inactive_account_deleted",
93
- )
77
+ mails.inactive_account_deleted().send(copied_user)
78
+
94
79
  logging.info(f"Deleted {users_to_delete.count()} inactive users")
@@ -6,14 +6,10 @@ from udata.core.followers.models import Follow
6
6
  from udata.core.organization.factories import OrganizationFactory
7
7
  from udata.core.user.factories import UserFactory
8
8
  from udata.core.user.models import User
9
+ from udata.tests.api import APITestCase
9
10
 
10
- pytestmark = pytest.mark.usefixtures("clean_db")
11
-
12
-
13
- @pytest.mark.frontend
14
- class UserModelTest:
15
- modules = [] # Required for mails
16
11
 
12
+ class UserModelTest(APITestCase):
17
13
  def test_mark_as_deleted(self):
18
14
  user = UserFactory()
19
15
  other_user = UserFactory()
udata/entrypoints.py CHANGED
@@ -4,12 +4,7 @@ import pkg_resources
4
4
  ENTRYPOINTS = {
5
5
  "udata.avatars": "Avatar rendering backends",
6
6
  "udata.harvesters": "Harvest backends",
7
- "udata.metrics": "Extra metrics",
8
7
  "udata.models": "Models and migrations",
9
- "udata.plugins": "Generic plugin",
10
- "udata.tasks": "Tasks and jobs",
11
- "udata.themes": "Themes",
12
- "udata.views": "Extra views",
13
8
  }
14
9
 
15
10
 
@@ -1,20 +1,10 @@
1
- import pytest
2
-
3
1
  from udata.features.identicon.backends import internal
2
+ from udata.tests.api import PytestOnlyAPITestCase
4
3
  from udata.tests.helpers import assert200
5
4
  from udata.utils import faker
6
5
 
7
- pytestmark = pytest.mark.usefixtures("app")
8
-
9
-
10
- def assert_stream_equal(response1, response2):
11
- __tracebackhide__ = True
12
- stream1 = list(response1.iter_encoded())
13
- stream2 = list(response2.iter_encoded())
14
- assert stream1 == stream2
15
-
16
6
 
17
- class InternalBackendTest:
7
+ class InternalBackendTest(PytestOnlyAPITestCase):
18
8
  def test_base_rendering(self):
19
9
  response = internal(faker.word(), 32)
20
10
  assert200(response)
@@ -25,4 +15,4 @@ class InternalBackendTest:
25
15
 
26
16
  def test_render_twice_the_same(self):
27
17
  identifier = faker.word()
28
- assert_stream_equal(internal(identifier, 32), internal(identifier, 32))
18
+ self.assertStreamEqual(internal(identifier, 32), internal(identifier, 32))
udata/forms/fields.py CHANGED
@@ -416,11 +416,11 @@ class TagField(Field):
416
416
  if not self.data:
417
417
  return
418
418
  for tag in self.data:
419
- if not tags.MIN_TAG_LENGTH <= len(tag) <= tags.MAX_TAG_LENGTH:
419
+ if not tags.TAG_MIN_LENGTH <= len(tag) <= tags.TAG_MAX_LENGTH:
420
420
  message = _(
421
421
  'Tag "%(tag)s" must be between %(min)d and %(max)d characters long.',
422
- min=tags.MIN_TAG_LENGTH,
423
- max=tags.MAX_TAG_LENGTH,
422
+ min=tags.TAG_MIN_LENGTH,
423
+ max=tags.TAG_MAX_LENGTH,
424
424
  tag=tag,
425
425
  )
426
426
  raise validators.ValidationError(message)
udata/forms/widgets.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from wtforms import widgets
2
2
 
3
- MIN_TAG_LENGTH = 2
4
- MAX_TAG_LENGTH = 50
3
+ TAG_MIN_LENGTH = 2
4
+ TAG_MAX_LENGTH = 50
5
5
 
6
6
 
7
7
  class WidgetHelper(object):
@@ -1,12 +1,8 @@
1
- import inspect
2
1
  import logging
3
- from importlib import import_module
4
2
 
5
- import pkg_resources
6
3
  from jinja2 import pass_context
7
4
  from markupsafe import Markup
8
5
 
9
- from udata import entrypoints
10
6
  from udata.i18n import I18nBlueprint
11
7
 
12
8
  from .markdown import UdataCleaner
@@ -20,11 +16,6 @@ hook = I18nBlueprint("hook", __name__)
20
16
  _template_hooks = {}
21
17
 
22
18
 
23
- @hook.app_template_global()
24
- def package_version(name: str) -> str:
25
- return pkg_resources.get_distribution(name).version
26
-
27
-
28
19
  @hook.app_template_filter()
29
20
  def avatar_placeholder(url):
30
21
  if url:
@@ -101,32 +92,12 @@ class SafeMarkup(Markup):
101
92
  return super().__new__(cls, cleaner.clean(base), *args, **kwargs)
102
93
 
103
94
 
104
- def _load_views(app, module):
105
- views = module if inspect.ismodule(module) else import_module(module)
106
- blueprint = getattr(views, "blueprint", None)
107
- if blueprint:
108
- app.register_blueprint(blueprint)
109
-
110
-
111
- VIEWS = ["core.storages"]
112
-
113
-
114
- def init_app(app, views=None):
115
- views = views or VIEWS
95
+ def init_app(app):
96
+ from udata.core.storages.views import blueprint as storage_blueprint
116
97
 
117
98
  init_markdown(app)
118
99
 
119
- for view in views:
120
- _load_views(app, "udata.{}.views".format(view))
100
+ app.register_blueprint(storage_blueprint)
121
101
 
122
102
  # Load hook blueprint
123
103
  app.register_blueprint(hook)
124
-
125
- # Load all plugins views and blueprints
126
- for module in entrypoints.get_enabled("udata.views", app).values():
127
- _load_views(app, module)
128
-
129
- # Load all plugins views and blueprints
130
- for module in entrypoints.get_enabled("udata.front", app).values():
131
- front_module = module if inspect.ismodule(module) else import_module(module)
132
- front_module.init_app(app)
udata/harvest/actions.py CHANGED
@@ -34,11 +34,6 @@ def get_source(ident):
34
34
  return HarvestSource.get(ident)
35
35
 
36
36
 
37
- def list_backends():
38
- """List all available backends"""
39
- return backends.get_all(current_app).values()
40
-
41
-
42
37
  def list_sources(owner=None, deleted=False):
43
38
  """List all harvest sources"""
44
39
  sources = HarvestSource.objects
@@ -177,7 +172,7 @@ def purge_jobs():
177
172
 
178
173
  def run(source: HarvestSource):
179
174
  """Launch or resume an harvesting for a given source if none is running"""
180
- cls = backends.get(current_app, source.backend)
175
+ cls = backends.get_backend(source.backend)
181
176
  backend = cls(source)
182
177
  backend.harvest()
183
178
 
@@ -189,7 +184,7 @@ def launch(source: HarvestSource):
189
184
 
190
185
  def preview(source: HarvestSource):
191
186
  """Preview an harvesting for a given source"""
192
- cls = backends.get(current_app, source.backend)
187
+ cls = backends.get_backend(source.backend)
193
188
  max_items = current_app.config["HARVEST_PREVIEW_MAX_ITEMS"]
194
189
  backend = cls(source, dryrun=True, max_items=max_items)
195
190
  return backend.harvest()
@@ -226,7 +221,7 @@ def preview_from_config(
226
221
  active=active,
227
222
  autoarchive=autoarchive,
228
223
  )
229
- cls = backends.get(current_app, source.backend)
224
+ cls = backends.get_backend(source.backend)
230
225
  max_items = current_app.config["HARVEST_PREVIEW_MAX_ITEMS"]
231
226
  backend = cls(source, dryrun=True, max_items=max_items)
232
227
  return backend.harvest()
@@ -273,7 +268,7 @@ def schedule(
273
268
  def unschedule(source: HarvestSource):
274
269
  """Unschedule an harvesting on a source"""
275
270
  if not source.periodic_task:
276
- msg = "Harvesting on source {0} is ot scheduled".format(source.name)
271
+ msg = "Harvesting on source {0} is not scheduled".format(source.name)
277
272
  raise ValueError(msg)
278
273
 
279
274
  source.periodic_task.delete()
udata/harvest/api.py CHANGED
@@ -10,6 +10,7 @@ from udata.core.dataset.permissions import OwnablePermission
10
10
  from udata.core.organization.api_fields import org_ref_fields
11
11
  from udata.core.organization.permissions import EditOrganizationPermission
12
12
  from udata.core.user.api_fields import user_ref_fields
13
+ from udata.harvest.backends import get_enabled_backends
13
14
 
14
15
  from . import actions
15
16
  from .forms import HarvestSourceForm, HarvestSourceValidationForm
@@ -25,10 +26,6 @@ from .models import (
25
26
  ns = api.namespace("harvest", "Harvest related operations")
26
27
 
27
28
 
28
- def backends_ids():
29
- return [b.name for b in actions.list_backends()]
30
-
31
-
32
29
  error_fields = api.model(
33
30
  "HarvestError",
34
31
  {
@@ -126,7 +123,9 @@ source_fields = api.model(
126
123
  "description": fields.Markdown(description="The source description"),
127
124
  "url": fields.String(description="The source base URL", required=True),
128
125
  "backend": fields.String(
129
- description="The source backend", enum=backends_ids, required=True
126
+ description="The source backend",
127
+ enum=lambda: list(get_enabled_backends().keys()),
128
+ required=True,
130
129
  ),
131
130
  "config": fields.Raw(description="The configuration as key-value pairs"),
132
131
  "created_at": fields.ISODateTime(
@@ -457,15 +456,7 @@ class ListBackendsAPI(API):
457
456
  "features": [f.as_dict() for f in b.features],
458
457
  "extra_configs": [f.as_dict() for f in b.extra_configs],
459
458
  }
460
- for b in actions.list_backends()
459
+ for b in get_enabled_backends().values()
461
460
  ],
462
461
  key=lambda b: b["label"],
463
462
  )
464
-
465
-
466
- @ns.route("/job_status/", endpoint="havest_job_status")
467
- class ListHarvesterAPI(API):
468
- @api.doc(model=[str])
469
- def get(self):
470
- """List all available harvesters"""
471
- return actions.list_backends()