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

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

Potentially problematic release.


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

Files changed (272) hide show
  1. udata/api/__init__.py +1 -0
  2. udata/api_fields.py +10 -4
  3. udata/app.py +11 -10
  4. udata/auth/__init__.py +9 -10
  5. udata/auth/mails.py +137 -45
  6. udata/auth/views.py +5 -12
  7. udata/commands/__init__.py +2 -4
  8. udata/commands/info.py +1 -3
  9. udata/commands/tests/test_fixtures.py +6 -3
  10. udata/core/access_type/api.py +18 -0
  11. udata/core/access_type/constants.py +98 -0
  12. udata/core/access_type/models.py +44 -0
  13. udata/core/activity/models.py +1 -1
  14. udata/core/badges/models.py +1 -1
  15. udata/core/badges/tasks.py +35 -1
  16. udata/core/badges/tests/test_commands.py +2 -4
  17. udata/core/badges/tests/test_model.py +2 -2
  18. udata/core/badges/tests/test_tasks.py +55 -0
  19. udata/core/constants.py +1 -0
  20. udata/core/contact_point/models.py +8 -0
  21. udata/core/dataservices/api.py +10 -12
  22. udata/core/dataservices/apiv2.py +3 -1
  23. udata/core/dataservices/constants.py +0 -29
  24. udata/core/dataservices/models.py +44 -44
  25. udata/core/dataservices/rdf.py +2 -1
  26. udata/core/dataservices/search.py +5 -9
  27. udata/core/dataservices/tasks.py +33 -0
  28. udata/core/dataset/api.py +15 -24
  29. udata/core/dataset/api_fields.py +11 -0
  30. udata/core/dataset/apiv2.py +11 -0
  31. udata/core/dataset/constants.py +0 -1
  32. udata/core/dataset/forms.py +29 -0
  33. udata/core/dataset/models.py +24 -42
  34. udata/core/dataset/rdf.py +2 -1
  35. udata/core/dataset/search.py +2 -2
  36. udata/core/dataset/tasks.py +86 -8
  37. udata/core/discussions/mails.py +63 -0
  38. udata/core/discussions/tasks.py +4 -18
  39. udata/core/metrics/__init__.py +0 -6
  40. udata/core/organization/api.py +20 -14
  41. udata/core/organization/mails.py +144 -0
  42. udata/core/organization/models.py +2 -1
  43. udata/core/organization/rdf.py +3 -3
  44. udata/core/organization/search.py +1 -1
  45. udata/core/organization/tasks.py +21 -49
  46. udata/core/pages/tests/test_api.py +0 -2
  47. udata/core/reuse/api.py +29 -3
  48. udata/core/reuse/mails.py +21 -0
  49. udata/core/reuse/models.py +10 -1
  50. udata/core/reuse/search.py +1 -1
  51. udata/core/reuse/tasks.py +2 -3
  52. udata/core/site/api.py +27 -19
  53. udata/core/site/models.py +2 -6
  54. udata/core/site/rdf.py +2 -2
  55. udata/core/spatial/tests/test_api.py +17 -20
  56. udata/core/spatial/tests/test_models.py +3 -3
  57. udata/core/user/mails.py +54 -0
  58. udata/core/user/models.py +2 -3
  59. udata/core/user/tasks.py +8 -23
  60. udata/core/user/tests/test_user_model.py +2 -6
  61. udata/entrypoints.py +0 -6
  62. udata/features/identicon/tests/test_backends.py +3 -13
  63. udata/forms/fields.py +3 -3
  64. udata/forms/widgets.py +2 -2
  65. udata/frontend/__init__.py +3 -32
  66. udata/harvest/actions.py +4 -9
  67. udata/harvest/api.py +5 -14
  68. udata/harvest/backends/__init__.py +20 -11
  69. udata/harvest/backends/base.py +2 -2
  70. udata/harvest/backends/ckan/harvesters.py +2 -1
  71. udata/harvest/backends/dcat.py +3 -0
  72. udata/harvest/backends/maaf.py +1 -0
  73. udata/harvest/commands.py +6 -4
  74. udata/harvest/forms.py +9 -6
  75. udata/harvest/tasks.py +3 -5
  76. udata/harvest/tests/ckan/test_ckan_backend.py +300 -337
  77. udata/harvest/tests/ckan/test_ckan_backend_errors.py +94 -99
  78. udata/harvest/tests/ckan/test_ckan_backend_filters.py +128 -122
  79. udata/harvest/tests/ckan/test_dkan_backend.py +39 -51
  80. udata/harvest/tests/dcat/bnodes.xml +17 -1
  81. udata/harvest/tests/dcat/datara--5a26b0f6-0ccf-46ad-ac58-734054b91977.rdf.xml +255 -0
  82. udata/harvest/tests/dcat/datara--f40c3860-7236-4b30-a141-23b8ae33f7b2.rdf.xml +289 -0
  83. udata/harvest/tests/factories.py +1 -1
  84. udata/harvest/tests/test_actions.py +11 -9
  85. udata/harvest/tests/test_api.py +4 -5
  86. udata/harvest/tests/test_base_backend.py +5 -4
  87. udata/harvest/tests/test_dcat_backend.py +72 -16
  88. udata/harvest/tests/test_models.py +2 -4
  89. udata/harvest/tests/test_notifications.py +2 -4
  90. udata/harvest/tests/test_tasks.py +2 -3
  91. udata/mail.py +90 -53
  92. udata/migrations/2025-01-05-dataservices-fields-changes.py +8 -14
  93. udata/migrations/2025-10-21-remove-ckan-harvest-modified-at.py +28 -0
  94. udata/migrations/2025-10-29-harvesters-sources-integrity.py +27 -0
  95. udata/models/__init__.py +0 -2
  96. udata/mongo/extras_fields.py +4 -3
  97. udata/mongo/taglist_field.py +3 -3
  98. udata/rdf.py +65 -20
  99. udata/sentry.py +3 -4
  100. udata/settings.py +15 -13
  101. udata/tags.py +5 -5
  102. udata/tasks.py +3 -3
  103. udata/templates/mail/message.html +65 -0
  104. udata/templates/mail/message.txt +16 -0
  105. udata/tests/__init__.py +40 -58
  106. udata/tests/api/__init__.py +87 -2
  107. udata/tests/api/test_activities_api.py +17 -23
  108. udata/tests/api/test_auth_api.py +2 -4
  109. udata/tests/api/test_contact_points.py +48 -54
  110. udata/tests/api/test_dataservices_api.py +65 -97
  111. udata/tests/api/test_datasets_api.py +171 -56
  112. udata/tests/api/test_me_api.py +4 -6
  113. udata/tests/api/test_organizations_api.py +19 -38
  114. udata/tests/api/test_reports_api.py +0 -4
  115. udata/tests/api/test_reuses_api.py +99 -23
  116. udata/tests/api/test_security_api.py +124 -0
  117. udata/tests/api/test_swagger.py +2 -3
  118. udata/tests/api/test_tags_api.py +6 -7
  119. udata/tests/api/test_transfer_api.py +0 -2
  120. udata/tests/api/test_user_api.py +8 -10
  121. udata/tests/apiv2/test_datasets.py +0 -4
  122. udata/tests/apiv2/test_me_api.py +0 -2
  123. udata/tests/apiv2/test_organizations.py +0 -2
  124. udata/tests/apiv2/test_swagger.py +2 -3
  125. udata/tests/apiv2/test_topics.py +0 -2
  126. udata/tests/cli/test_cli_base.py +14 -12
  127. udata/tests/cli/test_db_cli.py +51 -54
  128. udata/tests/contact_point/test_contact_point_models.py +2 -2
  129. udata/tests/dataservice/test_csv_adapter.py +2 -5
  130. udata/tests/dataservice/test_dataservice_rdf.py +64 -4
  131. udata/tests/dataservice/test_dataservice_tasks.py +36 -38
  132. udata/tests/dataset/test_csv_adapter.py +2 -5
  133. udata/tests/dataset/test_dataset_actions.py +2 -4
  134. udata/tests/dataset/test_dataset_commands.py +2 -4
  135. udata/tests/dataset/test_dataset_events.py +3 -3
  136. udata/tests/dataset/test_dataset_model.py +6 -7
  137. udata/tests/dataset/test_dataset_rdf.py +205 -16
  138. udata/tests/dataset/test_dataset_recommendations.py +2 -2
  139. udata/tests/dataset/test_dataset_tasks.py +66 -68
  140. udata/tests/dataset/test_resource_preview.py +39 -48
  141. udata/tests/dataset/test_transport_tasks.py +2 -2
  142. udata/tests/features/territories/__init__.py +0 -6
  143. udata/tests/features/territories/test_territories_api.py +25 -24
  144. udata/tests/forms/test_current_user_field.py +2 -2
  145. udata/tests/forms/test_dict_field.py +2 -4
  146. udata/tests/forms/test_extras_fields.py +2 -3
  147. udata/tests/forms/test_image_field.py +2 -2
  148. udata/tests/forms/test_model_field.py +2 -4
  149. udata/tests/forms/test_publish_as_field.py +2 -4
  150. udata/tests/forms/test_user_forms.py +26 -29
  151. udata/tests/frontend/test_auth.py +2 -3
  152. udata/tests/frontend/test_csv.py +5 -6
  153. udata/tests/frontend/test_error_handlers.py +2 -3
  154. udata/tests/frontend/test_hooks.py +5 -7
  155. udata/tests/frontend/test_markdown.py +3 -4
  156. udata/tests/helpers.py +2 -7
  157. udata/tests/metrics/test_metrics.py +52 -48
  158. udata/tests/metrics/test_tasks.py +154 -150
  159. udata/tests/organization/test_csv_adapter.py +2 -5
  160. udata/tests/organization/test_notifications.py +2 -4
  161. udata/tests/organization/test_organization_model.py +3 -4
  162. udata/tests/organization/test_organization_rdf.py +6 -12
  163. udata/tests/plugin.py +6 -110
  164. udata/tests/reuse/test_reuse_model.py +3 -4
  165. udata/tests/site/test_site_api.py +0 -2
  166. udata/tests/site/test_site_csv_exports.py +0 -2
  167. udata/tests/site/test_site_metrics.py +2 -4
  168. udata/tests/site/test_site_model.py +2 -2
  169. udata/tests/site/test_site_rdf.py +85 -29
  170. udata/tests/test_activity.py +3 -3
  171. udata/tests/test_api_fields.py +6 -9
  172. udata/tests/test_cors.py +0 -2
  173. udata/tests/test_dcat_commands.py +2 -3
  174. udata/tests/test_discussions.py +2 -7
  175. udata/tests/test_mail.py +150 -114
  176. udata/tests/test_migrations.py +413 -419
  177. udata/tests/test_model.py +10 -11
  178. udata/tests/test_notifications.py +2 -3
  179. udata/tests/test_owned.py +3 -3
  180. udata/tests/test_rdf.py +19 -15
  181. udata/tests/test_routing.py +5 -5
  182. udata/tests/test_storages.py +6 -5
  183. udata/tests/test_tags.py +2 -4
  184. udata/tests/test_topics.py +2 -4
  185. udata/tests/test_transfer.py +4 -5
  186. udata/tests/topic/test_topic_tasks.py +25 -27
  187. udata/tests/user/test_user_rdf.py +2 -8
  188. udata/tests/user/test_user_tasks.py +3 -5
  189. udata/tests/workers/test_jobs_commands.py +2 -2
  190. udata/tests/workers/test_tasks_routing.py +27 -27
  191. udata/translations/ar/LC_MESSAGES/udata.mo +0 -0
  192. udata/translations/ar/LC_MESSAGES/udata.po +369 -435
  193. udata/translations/de/LC_MESSAGES/udata.mo +0 -0
  194. udata/translations/de/LC_MESSAGES/udata.po +371 -437
  195. udata/translations/es/LC_MESSAGES/udata.mo +0 -0
  196. udata/translations/es/LC_MESSAGES/udata.po +369 -435
  197. udata/translations/fr/LC_MESSAGES/udata.mo +0 -0
  198. udata/translations/fr/LC_MESSAGES/udata.po +381 -447
  199. udata/translations/it/LC_MESSAGES/udata.mo +0 -0
  200. udata/translations/it/LC_MESSAGES/udata.po +371 -437
  201. udata/translations/pt/LC_MESSAGES/udata.mo +0 -0
  202. udata/translations/pt/LC_MESSAGES/udata.po +371 -437
  203. udata/translations/sr/LC_MESSAGES/udata.mo +0 -0
  204. udata/translations/sr/LC_MESSAGES/udata.po +372 -438
  205. udata/translations/udata.pot +379 -440
  206. udata/utils.py +66 -4
  207. {udata-12.0.2.dev10.dist-info → udata-13.0.1.dev21.dist-info}/METADATA +1 -4
  208. {udata-12.0.2.dev10.dist-info → udata-13.0.1.dev21.dist-info}/RECORD +212 -256
  209. udata/linkchecker/__init__.py +0 -0
  210. udata/linkchecker/backends.py +0 -31
  211. udata/linkchecker/checker.py +0 -75
  212. udata/linkchecker/commands.py +0 -21
  213. udata/linkchecker/models.py +0 -9
  214. udata/linkchecker/tasks.py +0 -55
  215. udata/templates/mail/account_deleted.html +0 -5
  216. udata/templates/mail/account_deleted.txt +0 -6
  217. udata/templates/mail/account_inactivity.html +0 -40
  218. udata/templates/mail/account_inactivity.txt +0 -31
  219. udata/templates/mail/badge_added_association.html +0 -33
  220. udata/templates/mail/badge_added_association.txt +0 -11
  221. udata/templates/mail/badge_added_certified.html +0 -33
  222. udata/templates/mail/badge_added_certified.txt +0 -11
  223. udata/templates/mail/badge_added_company.html +0 -33
  224. udata/templates/mail/badge_added_company.txt +0 -11
  225. udata/templates/mail/badge_added_local_authority.html +0 -33
  226. udata/templates/mail/badge_added_local_authority.txt +0 -11
  227. udata/templates/mail/badge_added_public_service.html +0 -33
  228. udata/templates/mail/badge_added_public_service.txt +0 -11
  229. udata/templates/mail/discussion_closed.html +0 -47
  230. udata/templates/mail/discussion_closed.txt +0 -16
  231. udata/templates/mail/inactive_account_deleted.html +0 -5
  232. udata/templates/mail/inactive_account_deleted.txt +0 -6
  233. udata/templates/mail/membership_refused.html +0 -20
  234. udata/templates/mail/membership_refused.txt +0 -11
  235. udata/templates/mail/membership_request.html +0 -46
  236. udata/templates/mail/membership_request.txt +0 -12
  237. udata/templates/mail/new_discussion.html +0 -44
  238. udata/templates/mail/new_discussion.txt +0 -15
  239. udata/templates/mail/new_discussion_comment.html +0 -45
  240. udata/templates/mail/new_discussion_comment.txt +0 -16
  241. udata/templates/mail/new_member.html +0 -27
  242. udata/templates/mail/new_member.txt +0 -11
  243. udata/templates/mail/new_reuse.html +0 -37
  244. udata/templates/mail/new_reuse.txt +0 -9
  245. udata/templates/mail/test.html +0 -6
  246. udata/templates/mail/test.txt +0 -6
  247. udata/templates/mail/user_mail_card.html +0 -26
  248. udata/templates/security/email/base.html +0 -105
  249. udata/templates/security/email/base.txt +0 -6
  250. udata/templates/security/email/button.html +0 -3
  251. udata/templates/security/email/change_notice.html +0 -22
  252. udata/templates/security/email/change_notice.txt +0 -8
  253. udata/templates/security/email/confirmation_instructions.html +0 -20
  254. udata/templates/security/email/confirmation_instructions.txt +0 -7
  255. udata/templates/security/email/login_instructions.html +0 -19
  256. udata/templates/security/email/login_instructions.txt +0 -7
  257. udata/templates/security/email/reset_instructions.html +0 -24
  258. udata/templates/security/email/reset_instructions.txt +0 -9
  259. udata/templates/security/email/reset_notice.html +0 -11
  260. udata/templates/security/email/reset_notice.txt +0 -4
  261. udata/templates/security/email/welcome.html +0 -24
  262. udata/templates/security/email/welcome.txt +0 -9
  263. udata/templates/security/email/welcome_existing.html +0 -32
  264. udata/templates/security/email/welcome_existing.txt +0 -14
  265. udata/terms.md +0 -6
  266. udata/tests/frontend/__init__.py +0 -23
  267. udata/tests/metrics/conftest.py +0 -15
  268. udata/tests/test_linkchecker.py +0 -277
  269. {udata-12.0.2.dev10.dist-info → udata-13.0.1.dev21.dist-info}/WHEEL +0 -0
  270. {udata-12.0.2.dev10.dist-info → udata-13.0.1.dev21.dist-info}/entry_points.txt +0 -0
  271. {udata-12.0.2.dev10.dist-info → udata-13.0.1.dev21.dist-info}/licenses/LICENSE +0 -0
  272. {udata-12.0.2.dev10.dist-info → udata-13.0.1.dev21.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,144 @@
1
+ from udata.core.organization.models import MembershipRequest, Organization
2
+ from udata.i18n import lazy_gettext as _
3
+ from udata.mail import LabelledContent, MailCTA, MailMessage, ParagraphWithLinks
4
+ from udata.uris import cdata_url
5
+
6
+
7
+ def new_membership_request(org: Organization, request: MembershipRequest) -> MailMessage:
8
+ return MailMessage(
9
+ subject=_("New membership request"),
10
+ paragraphs=[
11
+ ParagraphWithLinks(
12
+ _(
13
+ "You received a membership request from %(user)s for your organization %(org)s",
14
+ user=request.user,
15
+ org=org,
16
+ )
17
+ ),
18
+ LabelledContent(_("Reason for the request:"), request.comment),
19
+ MailCTA(_("See the request"), cdata_url(f"/admin/organizations/{org.id}/members/")),
20
+ ],
21
+ )
22
+
23
+
24
+ def membership_refused(org: Organization) -> MailMessage:
25
+ return MailMessage(
26
+ subject=_("Membership refused"),
27
+ paragraphs=[
28
+ ParagraphWithLinks(
29
+ _(
30
+ "Your membership for the organization %(org)s has been refused",
31
+ org=org,
32
+ )
33
+ ),
34
+ ],
35
+ )
36
+
37
+
38
+ def membership_accepted(org: Organization) -> MailMessage:
39
+ return MailMessage(
40
+ subject=_("Your invitation to join an organization has been accepted"),
41
+ paragraphs=[
42
+ ParagraphWithLinks(
43
+ _(
44
+ "Good news! Your request to join the organization %(org)s has been approved.",
45
+ org=org,
46
+ )
47
+ ),
48
+ MailCTA(
49
+ _("View the organization"), cdata_url(f"/admin/organizations/{org.id}/datasets")
50
+ ),
51
+ ],
52
+ )
53
+
54
+
55
+ def new_member(org: Organization) -> MailMessage:
56
+ return MailMessage(
57
+ subject=_("You have been added as a member of an organization"),
58
+ paragraphs=[
59
+ ParagraphWithLinks(
60
+ _(
61
+ "Good news! You are now a member of %(org)s.",
62
+ org=org,
63
+ )
64
+ ),
65
+ MailCTA(
66
+ _("View the organization"), cdata_url(f"/admin/organizations/{org.id}/datasets")
67
+ ),
68
+ ],
69
+ )
70
+
71
+
72
+ def badge_added_certified(org: Organization) -> MailMessage:
73
+ return MailMessage(
74
+ subject=_("Your organization has been certified"),
75
+ paragraphs=[
76
+ ParagraphWithLinks(
77
+ _(
78
+ "Good news! Your organization %(org)s has been certified by our team. A badge is now associated with your organization.",
79
+ org=org,
80
+ )
81
+ ),
82
+ MailCTA(_("View the organization"), org.self_web_url()),
83
+ ],
84
+ )
85
+
86
+
87
+ def badge_added_public_service(org: Organization) -> MailMessage:
88
+ return MailMessage(
89
+ subject=_("Your organization has been identified as a public service"),
90
+ paragraphs=[
91
+ ParagraphWithLinks(
92
+ _(
93
+ "Good news! Your organization %(org)s has been identified by our team as a public service. A badge is now associated with your organization.",
94
+ org=org,
95
+ )
96
+ ),
97
+ MailCTA(_("View the organization"), org.self_web_url()),
98
+ ],
99
+ )
100
+
101
+
102
+ def badge_added_local_authority(org: Organization) -> MailMessage:
103
+ return MailMessage(
104
+ subject=_("Your organization has been identified as a local authority"),
105
+ paragraphs=[
106
+ ParagraphWithLinks(
107
+ _(
108
+ "Good news! Your organization %(org)s has been identified by our team as a local authority. A badge is now associated with your organization.",
109
+ org=org,
110
+ )
111
+ ),
112
+ MailCTA(_("View the organization"), org.self_web_url()),
113
+ ],
114
+ )
115
+
116
+
117
+ def badge_added_company(org: Organization) -> MailMessage:
118
+ return MailMessage(
119
+ subject=_("Your organization has been identified as a company"),
120
+ paragraphs=[
121
+ ParagraphWithLinks(
122
+ _(
123
+ "Your organization %(org)s has been identified by our team as a company. A badge is now associated with your organization.",
124
+ org=org,
125
+ )
126
+ ),
127
+ MailCTA(_("View the organization"), org.self_web_url()),
128
+ ],
129
+ )
130
+
131
+
132
+ def badge_added_association(org: Organization) -> MailMessage:
133
+ return MailMessage(
134
+ subject=_("Your organization has been identified as an association"),
135
+ paragraphs=[
136
+ ParagraphWithLinks(
137
+ _(
138
+ "Your organization %(org)s has been identified by our team as an association. A badge is now associated with your organization.",
139
+ org=org,
140
+ )
141
+ ),
142
+ MailCTA(_("View the organization"), org.self_web_url()),
143
+ ],
144
+ )
@@ -3,6 +3,7 @@ from itertools import chain
3
3
 
4
4
  from blinker import Signal
5
5
  from flask import url_for
6
+ from flask_babel import LazyString
6
7
  from mongoengine.signals import post_save, pre_save
7
8
  from werkzeug.utils import cached_property
8
9
 
@@ -34,7 +35,7 @@ from .constants import (
34
35
 
35
36
  __all__ = ("Organization", "Team", "Member", "MembershipRequest")
36
37
 
37
- BADGES: dict[str, str] = {
38
+ BADGES: dict[str, LazyString] = {
38
39
  PUBLIC_SERVICE: _("Public Service"),
39
40
  CERTIFIED: _("Certified"),
40
41
  ASSOCIATION: _("Association"),
@@ -32,7 +32,7 @@ def organization_to_rdf(org, graph=None):
32
32
  return o
33
33
 
34
34
 
35
- def build_org_catalog(org, datasets, dataservices, format=None):
35
+ def build_org_catalog(org, datasets, dataservices, _format=None, **kwargs):
36
36
  graph = Graph(namespace_manager=namespace_manager)
37
37
  org_catalog_url = url_for("api.organization_rdf", org=org.id, _external=True)
38
38
 
@@ -47,9 +47,9 @@ def build_org_catalog(org, datasets, dataservices, format=None):
47
47
  for dataservice in dataservices:
48
48
  catalog.add(DCAT.service, dataservice_to_rdf(dataservice, graph))
49
49
 
50
- values = {"org": org.id}
50
+ values = {**kwargs, "org": org.id}
51
51
 
52
52
  if isinstance(datasets, Paginable):
53
- paginate_catalog(catalog, graph, datasets, format, "api.organization_rdf_format", **values)
53
+ paginate_catalog(catalog, graph, datasets, _format, "api.organization_rdf_format", **values)
54
54
 
55
55
  return catalog
@@ -29,7 +29,7 @@ class OrganizationSearch(search.ModelSearchAdapter):
29
29
  }
30
30
 
31
31
  @classmethod
32
- def is_indexable(cls, org):
32
+ def is_indexable(cls, org: Organization):
33
33
  return org.deleted is None
34
34
 
35
35
  @classmethod
@@ -1,11 +1,10 @@
1
- from udata import mail
2
1
  from udata.core import storages
3
2
  from udata.core.badges.tasks import notify_new_badge
4
- from udata.i18n import lazy_gettext as _
5
3
  from udata.models import Activity, ContactPoint, Dataset, Follow, Transfer
6
4
  from udata.search import reindex
7
5
  from udata.tasks import get_logger, job, task
8
6
 
7
+ from . import mails
9
8
  from .constants import ASSOCIATION, CERTIFIED, COMPANY, LOCAL_AUTHORITY, PUBLIC_SERVICE
10
9
  from .models import Organization
11
10
 
@@ -47,10 +46,11 @@ def notify_membership_request(org_id, request_id):
47
46
  org = Organization.objects.get(pk=org_id)
48
47
  request = next((r for r in org.requests if str(r.id) == request_id), None)
49
48
 
49
+ if request is None:
50
+ return
51
+
50
52
  recipients = [m.user for m in org.by_role("admin")]
51
- mail.send(
52
- _("New membership request"), recipients, "membership_request", org=org, request=request
53
- )
53
+ mails.new_membership_request(org, request).send(recipients)
54
54
 
55
55
 
56
56
  @task(route="high.mail")
@@ -58,12 +58,13 @@ def notify_membership_response(org_id, request_id):
58
58
  org = Organization.objects.get(pk=org_id)
59
59
  request = next((r for r in org.requests if str(r.id) == request_id), None)
60
60
 
61
+ if request is None:
62
+ return
63
+
61
64
  if request.status == "accepted":
62
- subject = _('You are now a member of the organization "%(org)s"', org=org)
63
- template = "new_member"
65
+ mails.membership_accepted(org).send(request.user)
64
66
  else:
65
- subject, template = _("Membership refused"), "membership_refused"
66
- mail.send(subject, request.user, template, org=org, request=request)
67
+ mails.membership_refused(org).send(request.user)
67
68
 
68
69
 
69
70
  @task
@@ -71,8 +72,10 @@ def notify_new_member(org_id, email):
71
72
  org = Organization.objects.get(pk=org_id)
72
73
  member = next((m for m in org.members if m.user.email == email), None)
73
74
 
74
- subject = _('You are now a member of the organization "%(org)s"', org=org)
75
- mail.send(subject, member.user, "new_member", org=org)
75
+ if member is None:
76
+ return
77
+
78
+ mails.new_member(org).send(member.user)
76
79
 
77
80
 
78
81
  @notify_new_badge(Organization, CERTIFIED)
@@ -82,14 +85,8 @@ def notify_badge_certified(org_id):
82
85
  """
83
86
  org = Organization.objects.get(pk=org_id)
84
87
  recipients = [member.user for member in org.members]
85
- subject = _('Your organization "%(name)s" has been certified', name=org.name)
86
- mail.send(
87
- subject,
88
- recipients,
89
- "badge_added_certified",
90
- organization=org,
91
- badge=org.get_badge(CERTIFIED),
92
- )
88
+
89
+ mails.badge_added_certified(org).send(recipients)
93
90
 
94
91
 
95
92
  @notify_new_badge(Organization, PUBLIC_SERVICE)
@@ -99,14 +96,8 @@ def notify_badge_public_service(org_id):
99
96
  """
100
97
  org = Organization.objects.get(pk=org_id)
101
98
  recipients = [member.user for member in org.members]
102
- subject = _('Your organization "%(name)s" has been identified as public service', name=org.name)
103
- mail.send(
104
- subject,
105
- recipients,
106
- "badge_added_public_service",
107
- organization=org,
108
- badge=org.get_badge(PUBLIC_SERVICE),
109
- )
99
+
100
+ mails.badge_added_public_service(org).send(recipients)
110
101
 
111
102
 
112
103
  @notify_new_badge(Organization, COMPANY)
@@ -116,10 +107,7 @@ def notify_badge_company(org_id):
116
107
  """
117
108
  org = Organization.objects.get(pk=org_id)
118
109
  recipients = [member.user for member in org.members]
119
- subject = _('Your organization "%(name)s" has been identified as a company', name=org.name)
120
- mail.send(
121
- subject, recipients, "badge_added_company", organization=org, badge=org.get_badge(COMPANY)
122
- )
110
+ mails.badge_added_company(org).send(recipients)
123
111
 
124
112
 
125
113
  @notify_new_badge(Organization, ASSOCIATION)
@@ -129,14 +117,7 @@ def notify_badge_association(org_id):
129
117
  """
130
118
  org = Organization.objects.get(pk=org_id)
131
119
  recipients = [member.user for member in org.members]
132
- subject = _('Your organization "%(name)s" has been identified as an association', name=org.name)
133
- mail.send(
134
- subject,
135
- recipients,
136
- "badge_added_association",
137
- organization=org,
138
- badge=org.get_badge(ASSOCIATION),
139
- )
120
+ mails.badge_added_association(org).send(recipients)
140
121
 
141
122
 
142
123
  @notify_new_badge(Organization, LOCAL_AUTHORITY)
@@ -146,13 +127,4 @@ def notify_badge_local_authority(org_id):
146
127
  """
147
128
  org = Organization.objects.get(pk=org_id)
148
129
  recipients = [member.user for member in org.members]
149
- subject = _(
150
- 'Your organization "%(name)s" has been identified as a local authority', name=org.name
151
- )
152
- mail.send(
153
- subject,
154
- recipients,
155
- "badge_added_local_authority",
156
- organization=org,
157
- badge=org.get_badge(LOCAL_AUTHORITY),
158
- )
130
+ mails.badge_added_local_authority(org).send(recipients)
@@ -6,8 +6,6 @@ from udata.tests.api import APITestCase
6
6
 
7
7
 
8
8
  class PageAPITest(APITestCase):
9
- modules = []
10
-
11
9
  def test_create_get_update(self):
12
10
  self.login()
13
11
  datasets = DatasetFactory.create_batch(3)
udata/core/reuse/api.py CHANGED
@@ -12,6 +12,7 @@ from udata.api_fields import patch, patch_and_save
12
12
  from udata.auth import admin_permission
13
13
  from udata.core.badges import api as badges_api
14
14
  from udata.core.badges.fields import badge_fields
15
+ from udata.core.dataservices.models import Dataservice
15
16
  from udata.core.dataset.api_fields import dataset_ref_fields
16
17
  from udata.core.followers.api import FollowAPI
17
18
  from udata.core.organization.models import Organization
@@ -24,7 +25,7 @@ from udata.core.storages.api import (
24
25
  from udata.frontend.markdown import md
25
26
  from udata.i18n import gettext as _
26
27
  from udata.models import Dataset
27
- from udata.utils import id_or_404
28
+ from udata.utils import get_rss_feed_list, id_or_404
28
29
 
29
30
  from .api_fields import (
30
31
  reuse_suggestion_fields,
@@ -143,7 +144,7 @@ class ReusesAtomFeedAPI(API):
143
144
  link=request.url_root,
144
145
  )
145
146
 
146
- reuses: list[Reuse] = Reuse.objects.visible().order_by("-created_at").limit(15)
147
+ reuses = get_rss_feed_list(Reuse.objects.visible(), "created_at")
147
148
  for reuse in reuses:
148
149
  author_name = None
149
150
  author_uri = None
@@ -227,7 +228,8 @@ class ReuseDatasetsAPI(API):
227
228
  dataset = Dataset.objects.get_or_404(id=id_or_404(request.json["id"]))
228
229
  except Dataset.DoesNotExist:
229
230
  msg = "Dataset {0} does not exists".format(request.json["id"])
230
- api.abort(404, msg)
231
+ return api.abort(404, msg)
232
+
231
233
  if dataset in reuse.datasets:
232
234
  return reuse
233
235
  reuse.datasets.append(dataset)
@@ -235,6 +237,30 @@ class ReuseDatasetsAPI(API):
235
237
  return reuse, 201
236
238
 
237
239
 
240
+ @ns.route("/<reuse:reuse>/dataservices/", endpoint="reuse_add_dataservice")
241
+ class ReuseDataservicesAPI(API):
242
+ @api.secure
243
+ @api.doc("reuse_add_dataservice", **common_doc)
244
+ @api.expect(Dataservice.__ref_fields__)
245
+ @api.response(200, "The dataservice is already present", Reuse.__read_fields__)
246
+ @api.marshal_with(Reuse.__read_fields__, code=201)
247
+ def post(self, reuse):
248
+ """Add a dataservice to a given reuse"""
249
+ if "id" not in request.json:
250
+ api.abort(400, "Expect a dataservice identifier")
251
+ try:
252
+ dataservice = Dataservice.objects.get_or_404(id=id_or_404(request.json["id"]))
253
+ except Dataservice.DoesNotExist:
254
+ msg = "Dataservice {0} does not exists".format(request.json["id"])
255
+ return api.abort(404, msg)
256
+
257
+ if dataservice in reuse.dataservices:
258
+ return reuse
259
+ reuse.dataservices.append(dataservice)
260
+ reuse.save()
261
+ return reuse, 201
262
+
263
+
238
264
  @ns.route("/badges/", endpoint="available_reuse_badges")
239
265
  class AvailableDatasetBadgesAPI(API):
240
266
  @api.doc("available_reuse_badges")
@@ -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/api.py CHANGED
@@ -6,7 +6,7 @@ from udata.auth import admin_permission
6
6
  from udata.core import csv
7
7
  from udata.core.dataservices.csv import DataserviceCsvAdapter
8
8
  from udata.core.dataservices.models import Dataservice
9
- from udata.core.dataset.api import DatasetApiParser
9
+ from udata.core.dataset.api import DatasetApiParser, catalog_parser
10
10
  from udata.core.dataset.csv import ResourcesCsvAdapter
11
11
  from udata.core.dataset.search import DatasetSearch
12
12
  from udata.core.dataset.tasks import get_queryset as get_csv_queryset
@@ -47,39 +47,47 @@ class SiteAPI(API):
47
47
  return current_site
48
48
 
49
49
 
50
- @api.route("/site/data.<format>", endpoint="site_dataportal")
50
+ @api.route("/site/data.<_format>", endpoint="site_dataportal")
51
51
  class SiteDataPortal(API):
52
- def get(self, format):
52
+ def get(self, _format):
53
53
  """Root RDF endpoint with content negociation handling"""
54
- url = url_for("api.site_rdf_catalog_format", format=format)
54
+ url = url_for("api.site_rdf_catalog_format", _format=_format)
55
55
  return redirect(url)
56
56
 
57
57
 
58
58
  @api.route("/site/catalog", endpoint="site_rdf_catalog")
59
59
  class SiteRdfCatalog(API):
60
+ @api.expect(catalog_parser)
60
61
  def get(self):
61
62
  """Root RDF endpoint with content negociation handling"""
62
- format = RDF_EXTENSIONS[negociate_content()]
63
- url = url_for("api.site_rdf_catalog_format", format=format)
63
+ _format = RDF_EXTENSIONS[negociate_content()]
64
+ # We sanitize the args used as kwargs in url_for
65
+ params = catalog_parser.parse_args()
66
+ url = url_for("api.site_rdf_catalog_format", _format=_format, **params)
64
67
  return redirect(url)
65
68
 
66
69
 
67
- @api.route("/site/catalog.<format>", endpoint="site_rdf_catalog_format")
70
+ @api.route("/site/catalog.<_format>", endpoint="site_rdf_catalog_format")
68
71
  class SiteRdfCatalogFormat(API):
69
- def get(self, format):
70
- params = multi_to_dict(request.args)
71
- page = int(params.get("page", 1))
72
- page_size = int(params.get("page_size", 100))
73
- datasets = Dataset.objects.visible()
74
- if "tag" in params:
75
- datasets = datasets.filter(tags=params.get("tag", ""))
76
- datasets = datasets.paginate(page, page_size)
77
- dataservices = Dataservice.objects.visible().filter_by_dataset_pagination(datasets, page)
78
-
79
- catalog = build_catalog(current_site, datasets, dataservices=dataservices, format=format)
72
+ @api.expect(catalog_parser)
73
+ def get(self, _format):
74
+ """
75
+ Return the RDF catalog in the requested format.
76
+ Filtering, sorting and paginating abilities apply to the datasets elements.
77
+ """
78
+ params = catalog_parser.parse_args()
79
+ datasets = DatasetApiParser.parse_filters(Dataset.objects.visible(), params)
80
+ datasets = datasets.paginate(params["page"], params["page_size"])
81
+ dataservices = Dataservice.objects.visible().filter_by_dataset_pagination(
82
+ datasets, params["page"]
83
+ )
84
+
85
+ catalog = build_catalog(
86
+ current_site, datasets, dataservices=dataservices, _format=_format, **params
87
+ )
80
88
  # bypass flask-restplus make_response, since graph_response
81
89
  # is handling the content negociation directly
82
- return make_response(*graph_response(catalog, format))
90
+ return make_response(*graph_response(catalog, _format))
83
91
 
84
92
 
85
93
  @api.route("/site/datasets.csv", endpoint="site_datasets_csv")
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
udata/core/site/rdf.py CHANGED
@@ -15,7 +15,7 @@ from udata.uris import homepage_url
15
15
  from udata.utils import Paginable
16
16
 
17
17
 
18
- def build_catalog(site, datasets, dataservices=[], format=None):
18
+ def build_catalog(site, datasets, dataservices=[], _format=None, **kwargs):
19
19
  """Build the DCAT catalog for this site"""
20
20
  catalog_url = url_for("api.site_rdf_catalog", _external=True)
21
21
  graph = Graph(namespace_manager=namespace_manager)
@@ -45,6 +45,6 @@ def build_catalog(site, datasets, dataservices=[], format=None):
45
45
  catalog.add(DCAT.service, rdf_dataservice)
46
46
 
47
47
  if isinstance(datasets, Paginable):
48
- paginate_catalog(catalog, graph, datasets, format, "api.site_rdf_catalog_format")
48
+ paginate_catalog(catalog, graph, datasets, _format, "api.site_rdf_catalog_format", **kwargs)
49
49
 
50
50
  return catalog