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
@@ -54,8 +54,8 @@ class DatasetSearch(ModelSearchAdapter):
54
54
  }
55
55
 
56
56
  @classmethod
57
- def is_indexable(cls, dataset):
58
- return dataset.deleted is None and dataset.archived is None and not dataset.private
57
+ def is_indexable(cls, dataset: Dataset):
58
+ return dataset.is_visible
59
59
 
60
60
  @classmethod
61
61
  def mongo_search(cls, args):
@@ -1,6 +1,6 @@
1
1
  import collections
2
2
  import os
3
- from datetime import datetime
3
+ from datetime import date, datetime
4
4
  from tempfile import NamedTemporaryFile
5
5
 
6
6
  from celery.utils.log import get_task_logger
@@ -9,9 +9,15 @@ from mongoengine import ValidationError
9
9
 
10
10
  from udata import models as udata_models
11
11
  from udata.core import csv, storages
12
+ from udata.core.badges import tasks as badge_tasks
13
+ from udata.core.constants import HVD
12
14
  from udata.core.dataservices.models import Dataservice
15
+ from udata.core.dataset.constants import INSPIRE
16
+ from udata.core.organization.constants import CERTIFIED, PUBLIC_SERVICE
17
+ from udata.core.organization.models import Organization
13
18
  from udata.harvest.models import HarvestJob
14
19
  from udata.models import Activity, Discussion, Follow, TopicElement, Transfer, db
20
+ from udata.storage.s3 import store_bytes
15
21
  from udata.tasks import job
16
22
 
17
23
  from .models import Checksum, CommunityResource, Dataset, Resource
@@ -85,12 +91,14 @@ def get_queryset(model_cls):
85
91
  return model_cls.objects.filter(**params).no_cache()
86
92
 
87
93
 
94
+ def get_resource_for_csv_export_model(model, dataset):
95
+ for resource in dataset.resources:
96
+ if resource.extras.get("csv-export:model", "") == model:
97
+ return resource
98
+
99
+
88
100
  def get_or_create_resource(r_info, model, dataset):
89
- resource = None
90
- for r in dataset.resources:
91
- if r.extras.get("csv-export:model", "") == model:
92
- resource = r
93
- break
101
+ resource = get_resource_for_csv_export_model(model, dataset)
94
102
  if resource:
95
103
  for k, v in r_info.items():
96
104
  setattr(resource, k, v)
@@ -121,11 +129,16 @@ def store_resource(csvfile, model, dataset):
121
129
  return get_or_create_resource(r_info, model, dataset)
122
130
 
123
131
 
124
- def export_csv_for_model(model, dataset):
132
+ def export_csv_for_model(model, dataset, replace: bool = False):
125
133
  model_cls = getattr(udata_models, model.capitalize(), None)
126
134
  if not model_cls:
127
135
  log.error("Unknow model %s" % model)
128
136
  return
137
+
138
+ fs_filename_to_remove = None
139
+ if existing_resource := get_resource_for_csv_export_model(model, dataset):
140
+ fs_filename_to_remove = existing_resource.fs_filename
141
+
129
142
  queryset = get_queryset(model_cls)
130
143
  adapter = csv.get_adapter(model_cls)
131
144
  if not adapter:
@@ -151,6 +164,10 @@ def export_csv_for_model(model, dataset):
151
164
  else:
152
165
  dataset.last_modified_internal = datetime.utcnow()
153
166
  dataset.save()
167
+ # remove previous catalog if exists and replace is True
168
+ if replace and fs_filename_to_remove:
169
+ storages.resources.delete(fs_filename_to_remove)
170
+ return resource
154
171
  finally:
155
172
  csvfile.close()
156
173
  os.unlink(csvfile.name)
@@ -179,7 +196,23 @@ def export_csv(self, model=None):
179
196
 
180
197
  models = (model,) if model else ALLOWED_MODELS
181
198
  for model in models:
182
- export_csv_for_model(model, dataset)
199
+ resource = export_csv_for_model(model, dataset, replace=True)
200
+
201
+ # If we are the first day of the month, archive today catalogs
202
+ if (
203
+ current_app.config["EXPORT_CSV_ARCHIVE_S3_BUCKET"]
204
+ and resource
205
+ and date.today().day == 1
206
+ ):
207
+ log.info(
208
+ f"Archiving {model} csv catalog on {current_app.config['EXPORT_CSV_ARCHIVE_S3_BUCKET']} bucket"
209
+ )
210
+ with storages.resources.open(resource.fs_filename, "rb") as f:
211
+ store_bytes(
212
+ bucket=current_app.config["EXPORT_CSV_ARCHIVE_S3_BUCKET"],
213
+ filename=f"{current_app.config['EXPORT_CSV_ARCHIVE_S3_FILENAME_PREFIX']}{resource.title}",
214
+ bytes=f.read(),
215
+ )
183
216
 
184
217
 
185
218
  @job("bind-tabular-dataservice")
@@ -213,3 +246,48 @@ def bind_tabular_dataservice(self):
213
246
  log.error(exc_info=e)
214
247
 
215
248
  log.info(f"Bound {datasets.count()} datasets to TabularAPI dataservice")
249
+
250
+
251
+ @badge_tasks.register(model=Dataset, badge=HVD)
252
+ def update_dataset_hvd_badge() -> None:
253
+ """
254
+ Update HVD badges to candidate datasets, based on the hvd tag.
255
+ Only datasets owned by certified and public service organizations are candidate to have a HVD badge.
256
+ """
257
+ if not current_app.config["HVD_SUPPORT"]:
258
+ log.error("You need to set HVD_SUPPORT if you want to update dataset hvd badge")
259
+ return
260
+ public_certified_orgs = (
261
+ Organization.objects(badges__kind=PUBLIC_SERVICE).filter(badges__kind=CERTIFIED).only("id")
262
+ )
263
+
264
+ datasets = Dataset.objects(
265
+ tags="hvd", badges__kind__ne="hvd", organization__in=public_certified_orgs
266
+ )
267
+ log.info(f"Adding HVD badge to {datasets.count()} datasets")
268
+ for dataset in datasets:
269
+ dataset.add_badge(HVD)
270
+
271
+ datasets = Dataset.objects(tags__nin=["hvd"], badges__kind="hvd")
272
+ log.info(f"Removing HVD badge from {datasets.count()} datasets")
273
+ for dataset in datasets:
274
+ dataset.remove_badge(HVD)
275
+
276
+
277
+ @badge_tasks.register(model=Dataset, badge=INSPIRE)
278
+ def update_dataset_inspire_badge() -> None:
279
+ """
280
+ Update INSPIRE badges to candidate datasets, based on the inspire tag.
281
+ """
282
+ if not current_app.config["INSPIRE_SUPPORT"]:
283
+ log.error("You need to set INSPIRE_SUPPORT if you want to update dataset INSPIRE badge")
284
+ return
285
+ datasets = Dataset.objects(tags="inspire", badges__kind__ne="inspire")
286
+ log.info(f"Adding INSPIRE badge to {datasets.count()} datasets")
287
+ for dataset in datasets:
288
+ dataset.add_badge(INSPIRE)
289
+
290
+ datasets = Dataset.objects(tags__nin=["inspire"], badges__kind="inspire")
291
+ log.info(f"Removing INSPIRE badge from {datasets.count()} datasets")
292
+ for dataset in datasets:
293
+ dataset.remove_badge(INSPIRE)
@@ -0,0 +1,63 @@
1
+ from udata.core.discussions.models import Discussion, Message
2
+ from udata.i18n import lazy_gettext as _
3
+ from udata.mail import LabelledContent, MailCTA, MailMessage, ParagraphWithLinks
4
+
5
+
6
+ def new_discussion(discussion: Discussion) -> MailMessage:
7
+ return MailMessage(
8
+ subject=_(
9
+ "A new discussion has been opened on your %(type)s",
10
+ type=discussion.subject.verbose_name,
11
+ ),
12
+ paragraphs=[
13
+ ParagraphWithLinks(
14
+ _(
15
+ "You have a new discussion from %(user_or_org)s on your %(type)s %(object)s",
16
+ user_or_org=discussion.organization or discussion.user,
17
+ type=discussion.subject.verbose_name,
18
+ object=discussion.subject,
19
+ )
20
+ ),
21
+ LabelledContent(_("Discussion title:"), discussion.title, inline=True),
22
+ LabelledContent(_("Comment:"), discussion.discussion[0].content),
23
+ MailCTA(_("Reply"), discussion.url_for()),
24
+ ],
25
+ )
26
+
27
+
28
+ def new_discussion_comment(discussion: Discussion, comment: Message) -> MailMessage:
29
+ return MailMessage(
30
+ subject=_("A new comment has been added to a discussion"),
31
+ paragraphs=[
32
+ ParagraphWithLinks(
33
+ _(
34
+ "You have a new comment from %(user_or_org)s on your %(type)s %(object)s",
35
+ user_or_org=comment.posted_by_org_or_user,
36
+ type=discussion.subject.verbose_name,
37
+ object=discussion.subject,
38
+ )
39
+ ),
40
+ LabelledContent(_("Discussion title:"), discussion.title, inline=True),
41
+ LabelledContent(_("Comment:"), comment.content),
42
+ MailCTA(_("Reply"), discussion.url_for()),
43
+ ],
44
+ )
45
+
46
+
47
+ def discussion_closed(discussion: Discussion, comment: Message | None) -> MailMessage:
48
+ return MailMessage(
49
+ subject=_("A discussion has been closed"),
50
+ paragraphs=[
51
+ ParagraphWithLinks(
52
+ _(
53
+ "The discussion you participated in on the %(type)s %(object)s has been closed by %(user_or_org)s.",
54
+ user_or_org=discussion.closed_by_org_or_user,
55
+ type=discussion.subject.verbose_name,
56
+ object=discussion.subject,
57
+ )
58
+ ),
59
+ LabelledContent(_("Discussion title:"), discussion.title, inline=True),
60
+ LabelledContent(_("Comment:"), comment.content) if comment else None,
61
+ MailCTA(_("View the discussion"), discussion.url_for()),
62
+ ],
63
+ )
@@ -1,7 +1,6 @@
1
- from udata import mail
2
- from udata.i18n import lazy_gettext as _
3
1
  from udata.tasks import connect, get_logger
4
2
 
3
+ from . import mails
5
4
  from .constants import NOTIFY_DISCUSSION_SUBJECTS
6
5
  from .models import Discussion
7
6
  from .signals import on_discussion_closed, on_new_discussion, on_new_discussion_comment
@@ -22,15 +21,7 @@ def owner_recipients(discussion):
22
21
  def notify_new_discussion(discussion_id):
23
22
  discussion = Discussion.objects.get(pk=discussion_id)
24
23
  if isinstance(discussion.subject, NOTIFY_DISCUSSION_SUBJECTS):
25
- recipients = owner_recipients(discussion)
26
- subject = _("Your %(type)s have a new discussion", type=discussion.subject.verbose_name)
27
- mail.send(
28
- subject,
29
- recipients,
30
- "new_discussion",
31
- discussion=discussion,
32
- message=discussion.discussion[0],
33
- )
24
+ mails.new_discussion(discussion).send(owner_recipients(discussion))
34
25
  else:
35
26
  log.warning("Unrecognized discussion subject type %s", type(discussion.subject))
36
27
 
@@ -42,11 +33,7 @@ def notify_new_discussion_comment(discussion_id, message=None):
42
33
  if isinstance(discussion.subject, NOTIFY_DISCUSSION_SUBJECTS):
43
34
  recipients = owner_recipients(discussion) + [m.posted_by for m in discussion.discussion]
44
35
  recipients = list({u.id: u for u in recipients if u != message.posted_by}.values())
45
- subject = _("%(user)s commented your discussion", user=message.posted_by_name)
46
-
47
- mail.send(
48
- subject, recipients, "new_discussion_comment", discussion=discussion, message=message
49
- )
36
+ mails.new_discussion_comment(discussion, message).send(recipients)
50
37
  else:
51
38
  log.warning("Unrecognized discussion subject type %s", type(discussion.subject))
52
39
 
@@ -58,7 +45,6 @@ def notify_discussion_closed(discussion_id, message=None):
58
45
  if isinstance(discussion.subject, NOTIFY_DISCUSSION_SUBJECTS):
59
46
  recipients = owner_recipients(discussion) + [m.posted_by for m in discussion.discussion]
60
47
  recipients = list({u.id: u for u in recipients if u != discussion.closed_by}.values())
61
- subject = _("A discussion has been closed")
62
- mail.send(subject, recipients, "discussion_closed", discussion=discussion, message=message)
48
+ mails.discussion_closed(discussion, message).send(recipients)
63
49
  else:
64
50
  log.warning("Unrecognized discussion subject type %s", type(discussion.subject))
@@ -1,6 +1,3 @@
1
- from udata import entrypoints
2
-
3
-
4
1
  def init_app(app):
5
2
  # Load all core metrics
6
3
  import udata.core.user.metrics # noqa
@@ -9,6 +6,3 @@ def init_app(app):
9
6
  import udata.core.dataset.metrics # noqa
10
7
  import udata.core.reuse.metrics # noqa
11
8
  import udata.core.followers.metrics # noqa
12
-
13
- # Load metrics from plugins
14
- entrypoints.get_enabled("udata.metrics", app)
@@ -49,7 +49,7 @@ from .forms import (
49
49
  from .models import Member, MembershipRequest, Organization
50
50
  from .permissions import EditOrganizationPermission, OrganizationPrivatePermission
51
51
  from .rdf import build_org_catalog
52
- from .tasks import notify_membership_request, notify_membership_response
52
+ from .tasks import notify_membership_request, notify_membership_response, notify_new_member
53
53
 
54
54
  DEFAULT_SORTING = "-created_at"
55
55
  SUGGEST_SORTING = "-metrics.followers"
@@ -472,6 +472,8 @@ class MemberAPI(API):
472
472
  org.count_members()
473
473
  org.save()
474
474
 
475
+ notify_new_member.delay(str(org.id), str(member.user.email))
476
+
475
477
  return member, 201
476
478
 
477
479
  @api.secure
@@ -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"),
@@ -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
@@ -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")