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
udata/rdf.py CHANGED
@@ -128,17 +128,29 @@ EU_HVD_CATEGORIES = {
128
128
  HVD_LEGISLATION = "http://data.europa.eu/eli/reg_impl/2023/138/oj"
129
129
  TAG_TO_EU_HVD_CATEGORIES = {slugify_tag(EU_HVD_CATEGORIES[uri]): uri for uri in EU_HVD_CATEGORIES}
130
130
 
131
+ INSPIRE_GEMET_THEME_NAMESPACE = "http://inspire.ec.europa.eu/theme"
132
+ INSPIRE_GEMET_SCHEME_URIS = [
133
+ INSPIRE_GEMET_THEME_NAMESPACE,
134
+ "http://www.eionet.europa.eu/gemet/inspire_themes",
135
+ ]
136
+
131
137
  AGENT_ROLE_TO_RDF_PREDICATE = {
132
138
  "contact": DCAT.contactPoint,
133
- "publisher": DCT.publisher,
134
139
  "creator": DCT.creator,
140
+ "publisher": DCT.publisher,
141
+ "rightsHolder": DCT.rightsHolder,
142
+ "custodian": GEODCAT.custodian,
143
+ "distributor": GEODCAT.distributor,
144
+ "originator": GEODCAT.originator,
145
+ "principalInvestigator": GEODCAT.principalInvestigator,
146
+ "processor": GEODCAT.processor,
147
+ "resourceProvider": GEODCAT.resourceProvider,
148
+ "user": GEODCAT.user,
135
149
  }
136
150
 
137
151
  # Map rdf contact point entity to role
138
152
  CONTACT_POINT_ENTITY_TO_ROLE = {
139
- DCAT.contactPoint: "contact",
140
- DCT.publisher: "publisher",
141
- DCT.creator: "creator",
153
+ predicate: role for role, predicate in AGENT_ROLE_TO_RDF_PREDICATE.items()
142
154
  }
143
155
 
144
156
 
@@ -303,16 +315,37 @@ def theme_labels_from_rdf(rdf):
303
315
  """
304
316
  Get theme labels to use as keywords.
305
317
  Map HVD keywords from known URIs resources if HVD support is activated.
318
+ Map INSPIRE keyword from known themes if INSPIRE support is activated.
319
+ - An INSPIRE dataset is a dataset with a theme INSPIRE encoded with gmd:descriptiveKeywords/gmd:MD_Keywords.
320
+ In DCAT, it is shown as a DCAT.theme with a SKOS.inScheme pointing to to the INSPIRE thesaurus.
321
+ We filter on this thesaurus based on its name (expecting "GEMET - INSPIRE themes, version 1.0") or its uri.
306
322
  """
307
323
  for theme in rdf.objects(DCAT.theme):
308
324
  if isinstance(theme, RdfResource):
325
+ label = rdf_value(theme, SKOS.prefLabel)
309
326
  uri = theme.identifier.toPython()
310
327
  if current_app.config["HVD_SUPPORT"] and uri in EU_HVD_CATEGORIES:
328
+ # Map label from EU HVD categories
311
329
  label = EU_HVD_CATEGORIES[uri]
312
330
  # Additionnally yield hvd keyword
313
331
  yield "hvd"
314
- else:
315
- label = rdf_value(theme, SKOS.prefLabel)
332
+ if current_app.config["INSPIRE_SUPPORT"]:
333
+ if uri.startswith(INSPIRE_GEMET_THEME_NAMESPACE):
334
+ yield "inspire"
335
+ else:
336
+ # Check if the theme belongs to the GEMET INSPIRE scheme
337
+ if scheme := theme.value(SKOS.inScheme):
338
+ scheme_title = (
339
+ rdf_value(scheme, DCT.title)
340
+ or rdf_value(scheme, SKOS.prefLabel)
341
+ or rdf_value(scheme, RDFS.label)
342
+ )
343
+ scheme_uri = scheme.identifier.toPython()
344
+ if (
345
+ scheme_title
346
+ and scheme_title.lower() == "gemet - inspire themes, version 1.0"
347
+ ) or scheme_uri in INSPIRE_GEMET_SCHEME_URIS:
348
+ yield "inspire"
316
349
  else:
317
350
  label = theme.toPython()
318
351
  if label:
@@ -326,6 +359,8 @@ def themes_from_rdf(rdf):
326
359
 
327
360
 
328
361
  def contact_points_from_rdf(rdf, prop, role, dataset):
362
+ if not dataset.organization and not dataset.owner:
363
+ return
329
364
  for contact_point in rdf.objects(prop):
330
365
  # Read contact point information
331
366
  if isinstance(contact_point, Literal):
@@ -338,7 +373,7 @@ def contact_points_from_rdf(rdf, prop, role, dataset):
338
373
  email = (
339
374
  rdf_value(contact_point, VCARD.hasEmail)
340
375
  or rdf_value(contact_point, VCARD.email)
341
- or rdf_value(contact_point, DCAT.email)
376
+ or None
342
377
  )
343
378
  email = email.replace("mailto:", "").strip() if email else None
344
379
  contact_form = rdf_value(contact_point, VCARD.hasUrl)
@@ -357,8 +392,6 @@ def contact_points_from_rdf(rdf, prop, role, dataset):
357
392
  # continue
358
393
 
359
394
  # Create of get contact point object
360
- if not dataset.organization and not dataset.owner:
361
- continue
362
395
  org_or_owner = {}
363
396
  if dataset.organization:
364
397
  org_or_owner = {"organization": dataset.organization}
@@ -393,14 +426,25 @@ def contact_points_to_rdf(contacts, graph=None):
393
426
  id = BNode()
394
427
 
395
428
  node = graph.resource(id)
396
- node.set(RDF.type, VCARD.Kind)
397
- if contact.name:
398
- node.set(VCARD.fn, Literal(contact.name))
399
- if contact.email:
400
- node.set(VCARD.hasEmail, URIRef(f"mailto:{contact.email}"))
401
- if contact.contact_form:
402
- node.set(VCARD.hasUrl, URIRef(contact.contact_form))
403
- yield node, AGENT_ROLE_TO_RDF_PREDICATE.get(contact.role, DCAT.contactPoint)
429
+ role = AGENT_ROLE_TO_RDF_PREDICATE.get(contact.role, DCAT.contactPoint)
430
+ # GeoDCAT-AP spec: Only contactPoint is a VCARD.Kind (like in DCAT). Other roles are FOAF.Agent.
431
+ if role == DCAT.contactPoint:
432
+ node.set(RDF.type, VCARD.Kind)
433
+ if contact.name:
434
+ node.set(VCARD.fn, Literal(contact.name))
435
+ if contact.email:
436
+ node.set(VCARD.hasEmail, URIRef(f"mailto:{contact.email}"))
437
+ if contact.contact_form:
438
+ node.set(VCARD.hasUrl, URIRef(contact.contact_form))
439
+ else:
440
+ node.set(RDF.type, FOAF.Agent)
441
+ node.set(FOAF.name, Literal(contact.name))
442
+ if contact.email:
443
+ node.set(FOAF.mbox, URIRef(f"mailto:{contact.email}"))
444
+ if contact.contact_form:
445
+ node.set(FOAF.page, URIRef(contact.contact_form))
446
+
447
+ yield node, role
404
448
 
405
449
 
406
450
  def primary_topic_identifier_from_rdf(graph: Graph, resource: RdfResource):
@@ -490,17 +534,18 @@ def escape_xml_illegal_chars(val, replacement="?"):
490
534
  return illegal_xml_chars_RE.sub(replacement, val)
491
535
 
492
536
 
493
- def paginate_catalog(catalog, graph, datasets, format, rdf_catalog_endpoint, **values):
537
+ def paginate_catalog(catalog, graph, datasets, _format, rdf_catalog_endpoint, **values):
494
538
  if not format:
495
539
  raise ValueError("Pagination requires format")
496
540
  catalog.add(RDF.type, HYDRA.Collection)
497
541
  catalog.set(HYDRA.totalItems, Literal(datasets.total))
498
542
  kwargs = {
499
- "format": format,
543
+ "_format": _format,
500
544
  "page_size": datasets.page_size,
501
545
  "_external": True,
502
546
  }
503
-
547
+ values.pop("page", None)
548
+ values.pop("page_size", None)
504
549
  kwargs.update(values)
505
550
 
506
551
  first_url = url_for(rdf_catalog_endpoint, page=1, **kwargs)
udata/sentry.py CHANGED
@@ -2,15 +2,14 @@ import logging
2
2
  import re
3
3
  import warnings
4
4
 
5
- import pkg_resources
6
5
  from werkzeug.exceptions import HTTPException
7
6
 
8
7
  from udata import entrypoints
9
8
  from udata.core.storages.api import UploadProgress
9
+ from udata.utils import get_udata_version
10
10
 
11
11
  from .app import UDataApp
12
12
  from .auth import PermissionDenied
13
- from .frontend import package_version
14
13
 
15
14
  log = logging.getLogger(__name__)
16
15
 
@@ -64,7 +63,7 @@ def init_app(app: UDataApp):
64
63
  dsn=app.config["SENTRY_PUBLIC_DSN"],
65
64
  integrations=[FlaskIntegration(), CeleryIntegration()],
66
65
  ignore_errors=list(exceptions),
67
- release=f"udata@{package_version('udata')}",
66
+ release=f"udata@{get_udata_version()}",
68
67
  environment=app.config.get("SITE_ID", None),
69
68
  # Set traces_sample_rate to 1.0 to capture 100%
70
69
  # of transactions for performance monitoring.
@@ -88,4 +87,4 @@ def init_app(app: UDataApp):
88
87
  if dist.version:
89
88
  sentry_sdk.set_tag(dist.project_name, dist.version)
90
89
  # Do not forget udata itself
91
- sentry_sdk.set_tag("udata", pkg_resources.get_distribution("udata").version)
90
+ sentry_sdk.set_tag("udata", get_udata_version())
udata/settings.py CHANGED
@@ -1,4 +1,3 @@
1
- import pkg_resources
2
1
  from kombu import Exchange, Queue
3
2
  from tlds import tld_set
4
3
 
@@ -173,11 +172,11 @@ class Defaults(object):
173
172
  SITE_AUTHOR_URL = None
174
173
  SITE_AUTHOR = "Udata"
175
174
  SITE_GITHUB_URL = "https://github.com/etalab/udata"
176
- SITE_TERMS_LOCATION = pkg_resources.resource_filename(__name__, "terms.md")
177
175
 
178
176
  UDATA_INSTANCE_NAME = "udata"
179
177
 
180
178
  PLUGINS = []
179
+ HARVESTER_BACKENDS = []
181
180
  THEME = None
182
181
 
183
182
  STATIC_DIRS = []
@@ -288,6 +287,8 @@ class Defaults(object):
288
287
 
289
288
  DELAY_BEFORE_REMINDER_NOTIFICATION = 30 # Days
290
289
 
290
+ DELAY_BEFORE_APPEARING_IN_RSS_FEED = 10 # Hours
291
+
291
292
  # Harvest settings
292
293
  ###########################################################################
293
294
  HARVEST_ENABLE_MANUAL_RUN = False
@@ -324,23 +325,18 @@ class Defaults(object):
324
325
  S3_ACCESS_KEY_ID = None
325
326
  S3_SECRET_ACCESS_KEY = None
326
327
 
327
- # Specific support for hvd (map HVD categories URIs to keywords)
328
+ # Specific support for hvd:
329
+ # - map HVD categories URIs to keywords
328
330
  HVD_SUPPORT = True
329
331
 
332
+ # Specific support for inspire:
333
+ # - add inspire keyword during harvest if GEMETE INSPIRE thesaurus is used in DCAT.theme
334
+ INSPIRE_SUPPORT = True
335
+
330
336
  ACTIVATE_TERRITORIES = False
331
337
  # The order is important to compute parents/children, smaller first.
332
338
  HANDLED_LEVELS = tuple()
333
339
 
334
- LINKCHECKING_ENABLED = True
335
- # Resource types ignored by linkchecker
336
- LINKCHECKING_UNCHECKED_TYPES = ("api",)
337
- LINKCHECKING_IGNORE_DOMAINS = []
338
- LINKCHECKING_IGNORE_PATTERNS = ["format=shp"]
339
- LINKCHECKING_MIN_CACHE_DURATION = 60 # in minutes
340
- LINKCHECKING_MAX_CACHE_DURATION = 1080 # in minutes (1 week)
341
- LINKCHECKING_UNAVAILABLE_THRESHOLD = 100
342
- LINKCHECKING_DEFAULT_LINKCHECKER = "no_check"
343
-
344
340
  # Ignore some endpoint from API tracking
345
341
  # By default ignore the 3 most called APIs
346
342
  TRACKING_BLACKLIST = [
@@ -535,6 +531,8 @@ class Defaults(object):
535
531
  "harvest",
536
532
  )
537
533
  EXPORT_CSV_DATASET_ID = None
534
+ EXPORT_CSV_ARCHIVE_S3_BUCKET = None # If this setting is set, an archive is uploaded to the corresponding S3 bucket every first day of the month (if export-csv is scheduled to run daily)
535
+ EXPORT_CSV_ARCHIVE_S3_FILENAME_PREFIX = "" # Useful to store the csv archives inside a subfolder of the bucket, ie setting 'csv-catalog-archives/'`
538
536
 
539
537
  # Autocomplete parameters
540
538
  #########################
@@ -642,6 +640,7 @@ class Testing(object):
642
640
  CELERY_TASK_EAGER_PROPAGATES = True
643
641
  TEST_WITH_PLUGINS = False
644
642
  PLUGINS = []
643
+ HARVESTER_BACKENDS = ["factory"]
645
644
  TEST_WITH_THEME = False
646
645
  THEME = "testing"
647
646
  CACHE_TYPE = "flask_caching.backends.null"
@@ -661,6 +660,9 @@ class Testing(object):
661
660
  } # Disables deliverability for email domain name
662
661
  PUBLISH_ON_RESOURCE_EVENTS = False
663
662
  HARVEST_ACTIVITY_USER_ID = None
663
+ SEARCH_SERVICE_API_URL = None
664
+ CDATA_BASE_URL = None
665
+ SCHEMA_CATALOG_URL = None
664
666
 
665
667
 
666
668
  class Debug(Defaults):
udata/tags.py CHANGED
@@ -2,8 +2,8 @@ from flask import current_app
2
2
  from slugify import slugify
3
3
  from werkzeug.local import LocalProxy
4
4
 
5
- MIN_TAG_LENGTH = LocalProxy(lambda: current_app.config["TAG_MIN_LENGTH"])
6
- MAX_TAG_LENGTH = LocalProxy(lambda: current_app.config["TAG_MAX_LENGTH"])
5
+ TAG_MIN_LENGTH = LocalProxy(lambda: current_app.config["TAG_MIN_LENGTH"])
6
+ TAG_MAX_LENGTH = LocalProxy(lambda: current_app.config["TAG_MAX_LENGTH"])
7
7
 
8
8
 
9
9
  def slug(value: str) -> str:
@@ -12,10 +12,10 @@ def slug(value: str) -> str:
12
12
 
13
13
  def normalize(value: str) -> str:
14
14
  value = slug(value)
15
- if len(value) < MIN_TAG_LENGTH:
15
+ if len(value) < TAG_MIN_LENGTH:
16
16
  value = ""
17
- elif len(value) > MAX_TAG_LENGTH:
18
- value = value[:MAX_TAG_LENGTH]
17
+ elif len(value) > TAG_MAX_LENGTH:
18
+ value = value[:TAG_MAX_LENGTH]
19
19
  return value
20
20
 
21
21
 
udata/tasks.py CHANGED
@@ -1,12 +1,11 @@
1
1
  import logging
2
+ from importlib.metadata import entry_points
2
3
  from urllib.parse import urlparse
3
4
 
4
5
  from celery import Celery, Task
5
6
  from celery.utils.log import get_task_logger
6
7
  from celerybeatmongo.schedulers import MongoScheduler
7
8
 
8
- from udata import entrypoints
9
-
10
9
  log = logging.getLogger(__name__)
11
10
 
12
11
 
@@ -176,6 +175,7 @@ def init_app(app):
176
175
  import udata.harvest.tasks # noqa
177
176
  import udata.db.tasks # noqa
178
177
 
179
- entrypoints.get_enabled("udata.tasks", app)
178
+ for ep in entry_points(group="udata.tasks"):
179
+ ep.load()
180
180
 
181
181
  return celery
@@ -0,0 +1,65 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{ message.subject }}</title>
7
+ </head>
8
+ <body style="font-family: Marianne, Helvetica, Arial, sans-serif; line-height: 1.6; color: #333; background-color: #ffffff; max-width: 600px; margin: 0 auto; padding: 20px;">
9
+ <div style="margin-bottom: 30px;">
10
+ <svg width="186" height="59" viewBox="0 0 186 59" fill="none" xmlns="http://www.w3.org/2000/svg">
11
+ <path d="M143.469 29.5086H145.86L152.728 46.4699L159.558 29.5086H161.986L154.208 48.6328H151.248L143.469 29.5086Z" fill="#3558A2"/>
12
+ <path d="M138.218 40.5126V29.5086H140.419V40.4746C140.419 46.1664 137.155 49.3917 132.26 49.3917C127.328 49.3917 124.026 46.1664 124.026 40.4746V29.5086H126.265V40.5126C126.265 44.8383 128.656 47.2668 132.26 47.2668C135.789 47.2668 138.218 44.8383 138.218 40.5126Z" fill="#3558A2"/>
13
+ <path d="M109.662 28.7498C115.809 28.7498 119.945 33.6067 119.945 39.0708C119.945 44.5348 115.809 49.3918 109.662 49.3918C103.553 49.3918 99.4167 44.5348 99.4167 39.0708C99.4167 33.6067 103.553 28.7498 109.662 28.7498ZM109.7 47.2669C114.215 47.2669 117.554 43.3965 117.554 39.0708C117.554 34.6692 114.215 30.8747 109.7 30.8747C105.033 30.8747 101.731 34.7071 101.731 39.0708C101.731 43.4344 105.033 47.2669 109.7 47.2669Z" fill="#3558A2"/>
14
+ <path d="M79.7158 52.1617C79.7158 50.1886 80.7024 48.6328 82.6755 47.1909C81.7269 46.5459 81.2716 45.5214 81.2716 44.4968C81.2716 43.017 82.1822 41.7269 83.6621 40.8541C81.9546 39.602 80.968 37.6288 80.968 35.5039C80.968 31.8991 83.8139 28.7118 88.2155 28.7118C89.5435 28.7118 90.7578 29.0153 91.7823 29.5086H98.9539V31.4817H94.1728C95.0076 32.6201 95.5009 34.024 95.5009 35.5039C95.5009 39.1087 92.655 42.296 88.2155 42.296C87.0771 42.296 86.0526 42.0684 85.1419 41.7269C84.0036 42.3719 83.3965 43.3205 83.3965 44.1553C83.3965 45.0281 83.8139 45.7111 85.2178 45.7111H91.0613C96.2598 45.7111 98.4227 48.1016 98.4227 51.4028C98.4227 55.2732 94.4764 58.1191 88.8605 58.1191C83.4724 58.1191 79.7158 55.8424 79.7158 52.1617ZM88.2534 40.3988C91.4787 40.3988 93.2621 38.0842 93.2621 35.5039C93.2621 32.8478 91.4787 30.609 88.2534 30.609C84.9522 30.609 83.1688 32.8478 83.1688 35.5039C83.1688 38.1221 84.9522 40.3988 88.2534 40.3988ZM81.9546 51.972C81.9546 54.5522 84.7625 56.1839 88.8226 56.1839C93.2242 56.1839 96.1839 54.4005 96.1839 51.5166C96.1839 49.4297 95.0456 47.7601 90.9854 47.7601H84.6487C82.9032 48.9364 81.9546 50.2645 81.9546 51.972Z" fill="#3558A2"/>
15
+ <path d="M65.9257 49.2021C62.0933 49.2021 59.4751 47.0013 59.4751 43.4724C59.4751 40.5886 61.7138 38.4257 65.8119 37.7427L71.6554 36.7561V36.2628C71.6554 34.2518 70.1376 32.9616 67.9368 32.9616C66.0775 32.9616 64.6356 33.8344 63.6111 35.2383L60.0822 32.5442C61.7897 30.1917 64.5977 28.7498 68.0886 28.7498C73.6285 28.7498 76.4744 32.051 76.4744 36.2628V48.6329H71.6554V46.7736C70.4412 48.2534 68.1645 49.2021 65.9257 49.2021ZM64.2562 43.2447C64.2562 44.5348 65.2807 45.3696 66.9123 45.3696C69.1131 45.3696 70.7068 44.3451 71.6554 42.8273V40.1332L67.102 40.8921C65.0909 41.2336 64.2562 42.0684 64.2562 43.2447Z" fill="#3558A2"/>
16
+ <path d="M46.6593 41.6509V33.8343H43.0925V29.5086H46.6593V24.7275H51.5163V29.5086H57.3598V33.8343H51.5163V41.6509C51.5163 43.7759 52.6546 44.6106 54.5519 44.6106C55.88 44.6106 56.7527 44.4589 57.3978 44.1932V48.4051C56.4491 48.8225 55.3108 49.0123 53.7171 49.0123C48.936 49.0123 46.6593 46.3182 46.6593 41.6509Z" fill="#3558A2"/>
17
+ <path d="M30.5161 49.2021C26.6836 49.2021 24.0654 47.0013 24.0654 43.4724C24.0654 40.5886 26.3042 38.4257 30.4022 37.7427L36.2457 36.7561V36.2628C36.2457 34.2518 34.7279 32.9616 32.5271 32.9616C30.6678 32.9616 29.2259 33.8344 28.2014 35.2383L24.6725 32.5442C26.3801 30.1917 29.188 28.7498 32.6789 28.7498C38.2189 28.7498 41.0647 32.051 41.0647 36.2628V48.6329H36.2457V46.7736C35.0315 48.2534 32.7548 49.2021 30.5161 49.2021ZM28.8465 43.2447C28.8465 44.5348 29.871 45.3696 31.5026 45.3696C33.7034 45.3696 35.2971 44.3451 36.2457 42.8273V40.1332L31.6924 40.8921C29.6813 41.2336 28.8465 42.0684 28.8465 43.2447Z" fill="#3558A2"/>
18
+ <path d="M0 39.0706C0 33.4927 3.68066 28.7496 9.71389 28.7496C12.2941 28.7496 14.1534 29.5465 15.6333 31.0263V20.1741H20.4902V48.6327H15.6333V47.115C14.1534 48.5948 12.2941 49.3916 9.71389 49.3916C3.68066 49.3916 0 44.6485 0 39.0706ZM5.04667 39.0706C5.04667 42.4098 7.13364 44.8383 10.3969 44.8383C12.5598 44.8383 14.3432 43.9276 15.6333 42.1821V35.9591C14.3432 34.2137 12.5598 33.303 10.3969 33.303C7.13364 33.303 5.04667 35.7315 5.04667 39.0706Z" fill="#3558A2"/>
19
+ <path d="M179.017 11.7431C179.017 11.7431 179.017 11.7297 179.017 11.7207C179.008 10.9944 178.619 10.3218 177.994 9.96316L174.864 8.1518C174.573 7.98142 174.238 7.89175 173.894 7.88278C173.845 7.88278 173.8 7.88278 173.751 7.88727C173.509 7.90072 173.281 7.94555 173.067 8.03074C172.982 8.06661 172.901 8.10696 172.821 8.1518L170.916 9.25476L169.731 9.94074C169.66 9.98109 169.593 10.0259 169.53 10.0752C169.53 10.0752 169.503 10.0932 169.499 10.0932C169.248 10.277 169.052 10.5146 168.913 10.7792C168.904 10.7971 168.891 10.815 168.882 10.833C168.873 10.8509 168.864 10.8688 168.859 10.8868C168.734 11.1603 168.667 11.4696 168.667 11.7969V15.2672V15.321C168.667 15.3838 168.672 15.4511 168.681 15.5318C168.681 15.5721 168.69 15.6169 168.699 15.6663C168.734 15.9084 168.815 16.137 168.927 16.3433C169.11 16.684 169.383 16.9665 169.718 17.1593L172.794 18.9393C172.906 19.002 172.991 19.0424 173.076 19.0783C173.29 19.1679 173.518 19.2173 173.751 19.2262C173.76 19.2262 173.769 19.2262 173.777 19.2262C173.997 19.2307 174.22 19.2083 174.435 19.141C174.582 19.0962 174.73 19.0334 174.864 18.9527L177.971 17.1548C178.289 16.971 178.552 16.7065 178.736 16.3881C178.919 16.0698 179.017 15.7066 179.017 15.3434V11.7431Z" fill="#3558A2"/>
20
+ <path d="M168.667 18.4043V22.8378V24.1148L173.602 26.9722V21.2617L172.451 20.5965L168.667 18.4043Z" fill="#3558A2"/>
21
+ <path d="M178.359 9.68272C178.359 9.68272 178.4 9.71397 178.418 9.72736C178.418 9.72736 178.427 9.73183 178.431 9.73629C178.462 9.75862 178.494 9.78541 178.525 9.80773C178.525 9.80773 178.53 9.8122 178.534 9.81666C178.539 9.82112 178.548 9.82559 178.552 9.83005C178.588 9.86131 178.628 9.89256 178.66 9.92382C178.669 9.93275 178.678 9.94167 178.687 9.9506C178.709 9.97293 178.732 9.99525 178.754 10.0176C178.763 10.0265 178.772 10.0354 178.776 10.0399C178.808 10.0756 178.839 10.1113 178.871 10.1471C178.889 10.1649 178.902 10.1872 178.915 10.2051C178.924 10.2185 178.938 10.2319 178.947 10.2453C178.956 10.2542 178.965 10.2676 178.974 10.2765C179.005 10.3167 179.032 10.3614 179.059 10.406C179.086 10.4507 179.113 10.4908 179.14 10.5355C179.167 10.5846 179.189 10.6337 179.216 10.6828C179.238 10.7275 179.256 10.7677 179.274 10.8123C179.292 10.8525 179.306 10.8971 179.319 10.9373C179.319 10.9463 179.324 10.9507 179.328 10.9597C179.328 10.9686 179.333 10.9775 179.337 10.982C179.351 11.0222 179.364 11.0623 179.377 11.1025C179.395 11.1695 179.409 11.2365 179.418 11.3079C179.418 11.3079 179.418 11.3124 179.418 11.3168C179.418 11.3258 179.422 11.3392 179.422 11.3481C179.422 11.3704 179.431 11.3883 179.436 11.4106C179.449 11.5133 179.458 11.6204 179.458 11.7276V15.1968L180.149 15.5941V9.88363L175.179 7.02615V7.84321L178.207 9.58449C178.256 9.61128 178.306 9.64253 178.355 9.67825L178.359 9.68272Z" fill="#3558A2"/>
22
+ <path d="M161.984 7.02615V12.7595L166.954 15.6284V9.89506L161.984 7.02615Z" fill="#3558A2"/>
23
+ <path d="M178.969 16.8342C178.929 16.8925 178.884 16.9419 178.84 16.9958C178.809 17.0317 178.777 17.0676 178.746 17.1035C178.697 17.1573 178.639 17.2022 178.586 17.2516C178.559 17.274 178.532 17.3009 178.505 17.3279L179.352 17.8215C179.419 17.8619 179.464 17.9337 179.464 18.0145V22.8032L180.15 23.2026V17.4625L179.005 16.7938C179.005 16.7938 178.982 16.8207 178.973 16.8342H178.969Z" fill="#3558A2"/>
24
+ <path d="M172.779 6.64593V7.67751C172.779 7.67751 172.811 7.66858 172.828 7.65965C172.917 7.61946 173.015 7.5882 173.109 7.5614C173.185 7.53908 173.26 7.52121 173.341 7.50782C173.416 7.49442 173.488 7.48102 173.563 7.47209C173.59 7.47209 173.612 7.46316 173.639 7.45869V6.11451L168.704 3.25644V4.15405L172.668 6.44944C172.735 6.48963 172.779 6.56108 172.779 6.64146V6.64593Z" fill="#3558A2"/>
25
+ <path d="M161.984 20.3141L166.954 23.1686V17.4551L161.984 14.6006V20.3141Z" fill="#3558A2"/>
26
+ <path d="M185.53 20.314V14.6005L180.594 17.455V23.1685L185.53 20.314Z" fill="#3558A2"/>
27
+ <path d="M174.048 26.9723L179.018 24.1148V22.9138V18.4043L174.048 21.2618V26.9723Z" fill="#3558A2"/>
28
+ <path d="M167.4 15.6284L168.215 15.1533V11.7554C168.215 10.9754 168.585 10.2313 169.209 9.76061C169.209 9.76061 169.217 9.75613 169.222 9.75165C169.222 9.75165 169.226 9.75165 169.231 9.74716C169.24 9.74268 169.249 9.73372 169.253 9.73372C169.333 9.67544 169.391 9.6351 169.458 9.59475L172.335 7.92272V7.02618L167.4 9.89509V15.6284Z" fill="#3558A2"/>
29
+ <path d="M185.53 12.7595V7.02618L180.594 9.89509V15.6284L185.53 12.7595Z" fill="#3558A2"/>
30
+ <path d="M174.169 7.46644C174.218 7.47089 174.262 7.48426 174.311 7.48871C174.369 7.49762 174.431 7.51099 174.489 7.52435C174.543 7.53772 174.592 7.55554 174.641 7.57336C174.681 7.58672 174.721 7.59563 174.761 7.609V6.63336C174.761 6.55317 174.806 6.48189 174.872 6.44179L179.055 4.02718V3.25647L174.12 6.10767V7.45753C174.12 7.45753 174.155 7.46644 174.173 7.46644H174.169Z" fill="#3558A2"/>
31
+ <path d="M168.26 17.9037C168.273 17.8815 168.296 17.8637 168.318 17.8459C168.323 17.8459 168.327 17.837 168.332 17.8325L169.182 17.343C169.182 17.343 169.146 17.3074 169.124 17.2896C169.052 17.2273 168.981 17.165 168.914 17.0937C168.882 17.0626 168.855 17.027 168.829 16.9958C168.775 16.9335 168.726 16.8712 168.681 16.8045C168.672 16.7911 168.658 16.7778 168.645 16.7599L168.623 16.7733L167.4 17.4765V23.1688L168.22 22.697V18.0239C168.22 17.9883 168.233 17.9527 168.246 17.9216C168.246 17.9127 168.255 17.9082 168.26 17.8993V17.9037Z" fill="#3558A2"/>
32
+ <path d="M172.132 6.629L167.182 3.76953L162.227 6.629L167.182 9.49294L172.132 6.629Z" fill="#3558A2"/>
33
+ <path d="M178.847 2.86394L173.878 0L168.908 2.86394L173.878 5.72341L178.847 2.86394Z" fill="#3558A2"/>
34
+ <path d="M180.045 9.30052L180.371 9.49294L185.326 6.629L180.371 3.76953L179.396 4.33337L175.421 6.629L177.514 7.8417L180.045 9.30052Z" fill="#3558A2"/>
35
+ <path d="M180.264 16.1917L179.438 15.7158C179.433 15.7469 179.42 15.7825 179.415 15.8136C179.402 15.8892 179.384 15.9648 179.366 16.036C179.353 16.0805 179.335 16.125 179.321 16.165C179.299 16.2317 179.277 16.294 179.245 16.3607C179.236 16.3785 179.232 16.3918 179.228 16.4096L180.376 17.0679L185.328 14.2213L184.617 13.8121L180.492 16.1828C180.457 16.2006 180.421 16.2139 180.381 16.2139C180.34 16.2139 180.305 16.205 180.269 16.1828L180.264 16.1917Z" fill="#3558A2"/>
36
+ <path d="M175.068 19.3326C174.902 19.426 174.723 19.5016 174.544 19.5594C174.419 19.5994 174.29 19.6217 174.16 19.6395C174.115 19.6439 174.071 19.6439 174.026 19.6484C173.959 19.6528 173.896 19.6617 173.829 19.6617C173.793 19.6617 173.757 19.6617 173.722 19.6617C173.722 19.6617 173.717 19.6617 173.713 19.6617C173.57 19.6573 173.431 19.6395 173.292 19.6083C173.154 19.5772 173.02 19.5372 172.89 19.4838C172.787 19.4393 172.684 19.3904 172.581 19.337L169.585 17.6157L169.349 17.7536L168.875 18.0249L173.829 20.8715L174.366 20.5602L174.871 20.2711L175.881 19.6884L178.779 18.0249L178.305 17.7536L178.068 17.6157L175.072 19.337L175.068 19.3326Z" fill="#3558A2"/>
37
+ <path d="M168.256 15.7246C168.256 15.6935 168.247 15.6668 168.242 15.6357L167.29 16.1828C167.254 16.2006 167.218 16.2139 167.178 16.2139C167.138 16.2139 167.102 16.205 167.066 16.1828L162.938 13.8121L162.227 14.2213L167.182 17.0679L168.43 16.3518C168.354 16.1739 168.296 15.9693 168.26 15.7424C168.26 15.7424 168.26 15.7335 168.26 15.7291L168.256 15.7246Z" fill="#3558A2"/>
38
+ </svg>
39
+ </div>
40
+
41
+ <p style="margin-bottom: 20px;">{{ _('Hi %(user)s', user=recipient.first_name) }},</p>
42
+
43
+ {% for item in message.paragraphs %}
44
+ {% if item.__class__.__name__ == 'MailCTA' %}
45
+ <p style="margin: 25px 0;">
46
+ <a href="{{ item.link }}" style="color: #3558A2; text-decoration: underline; font-weight: 700;">{{ item.label }}</a>
47
+ </p>
48
+ {% elif item.__class__.__name__ == 'LabelledContent' %}
49
+ {% if item.inline %}
50
+ <p style="margin-bottom: 15px;"><strong>{{ item.label }}</strong> {{ item.truncated_content }}</p>
51
+ {% else %}
52
+ <p style="margin-bottom: 5px; font-weight: bold;">{{ item.label }}</p>
53
+ <p style="margin-bottom: 15px; margin-top: 0;">{{ item.truncated_content }}</p>
54
+ {% endif %}
55
+ {% elif item.__class__.__name__ == 'ParagraphWithLinks' %}
56
+ <p style="margin-bottom: 15px;">{{ item.html|safe }}</p>
57
+ {% else %}
58
+ <p style="margin-bottom: 15px;">{{ item }}</p>
59
+ {% endif %}
60
+ {% endfor %}
61
+
62
+ <p style="margin-top: 30px; margin-bottom: 5px;">{{ _('Have a great day') }},</p>
63
+ <p style="margin: 0;">{{ _('The %(site)s team', site=config.SITE_TITLE) }}</p>
64
+ </body>
65
+ </html>
@@ -0,0 +1,16 @@
1
+ {{ _('Hi %(user)s', user=recipient.first_name) }},
2
+
3
+ {% for item in message.paragraphs %}
4
+ {% if item.__class__.__name__ == 'MailCTA' %}
5
+ {{ item.label }}: {{ item.link }}
6
+ {% elif item.__class__.__name__ == 'LabelledContent' %}
7
+ {% if item.inline %}{{ item.label }} {{ item.truncated_content }}{% else %}{{ item.label }}
8
+ {{ item.truncated_content }}{% endif %}
9
+ {% else %}
10
+ {{ item }}
11
+ {% endif %}
12
+
13
+ {% endfor %}
14
+
15
+ {{ _('Have a great day') }},
16
+ {{ _('The %(site)s team', site=config.SITE_TITLE) }}
udata/tests/__init__.py CHANGED
@@ -1,24 +1,48 @@
1
1
  import unittest
2
2
 
3
3
  import pytest
4
+ from werkzeug import Response
4
5
 
5
6
  from udata import settings
7
+ from udata.app import UDataApp, create_app
8
+ from udata.tests.plugin import TestClient
6
9
 
7
10
  from . import helpers
8
11
 
9
12
 
10
- class TestCase(unittest.TestCase):
11
- settings = settings.Testing
13
+ class TestCaseMixin:
14
+ app: UDataApp
12
15
 
13
- @pytest.fixture(autouse=True)
14
- def inject_app(self, app):
15
- self.app = app
16
- return self.create_app()
17
-
18
- def create_app(self):
16
+ def get_settings(self, request):
19
17
  """
20
- Here for compatibility legacy test classes
18
+ This seems really complicated for what it's doing. I think we want to create
19
+ an app with the default setting being `settings.Testing` and some overrides
20
+ from the "options" markers. But to do that `settings.Testing` should inherit from
21
+ `settings.Default`?
22
+
23
+ We may also want to prevent loading `udata.cfg` for testing to avoid failing tests
24
+ locally because some config is changed on our computer. Tests should work only with
25
+ default settings (or overrides for a specific test) but not with a local `udata.cfg`.
26
+
27
+ Not sure if the plugin situation is still relevant now that plugins are integrated
28
+ into udata.
21
29
  """
30
+ _settings = settings.Testing
31
+ # apply the options(plugins) marker from pytest_flask as soon as app is created
32
+ # https://github.com/pytest-dev/pytest-flask/blob/a62ea18cb0fe89e3f3911192ab9ea4f9b12f8a16/pytest_flask/plugin.py#L126
33
+ # this lets us have default settings for plugins applied while testing
34
+ plugins = getattr(_settings, "PLUGINS", [])
35
+ for options in request.node.iter_markers("options"):
36
+ option = options.kwargs.get("plugins", []) or options.kwargs.get("PLUGINS", [])
37
+ plugins += option
38
+ setattr(_settings, "PLUGINS", plugins)
39
+ return _settings
40
+
41
+ @pytest.fixture(autouse=True, name="app")
42
+ def _app(self, request):
43
+ test_settings = self.get_settings(request)
44
+ self.app = create_app(settings.Defaults, override=test_settings)
45
+ self.app.test_client_class = TestClient
22
46
  return self.app
23
47
 
24
48
  def assertEqualDates(self, datetime1, datetime2, limit=1): # Seconds.
@@ -26,58 +50,16 @@ class TestCase(unittest.TestCase):
26
50
  __tracebackhide__ = True
27
51
  helpers.assert_equal_dates(datetime1, datetime2, limit=1)
28
52
 
29
-
30
- class WebTestMixin(object):
31
- user = None
32
-
33
- @pytest.fixture(autouse=True)
34
- def inject_client(self, client):
35
- """
36
- Inject test client for compatibility with Flask-Testing.
37
- """
38
- self.client = client
39
-
40
- def get(self, url, **kwargs):
41
- return self.client.get(url, **kwargs)
42
-
43
- def post(self, url, data=None, **kwargs):
44
- return self.client.post(url, data=data, **kwargs)
45
-
46
- def put(self, url, data=None, **kwargs):
47
- return self.client.put(url, data=data, **kwargs)
48
-
49
- def delete(self, url, data=None, **kwargs):
50
- return self.client.delete(url, data=data, **kwargs)
51
-
52
- def assertRedirects(self, response, location, message=None):
53
- """
54
- Checks if response is an HTTP redirect to the
55
- given location.
56
- :param response: Flask response
57
- :param location: relative URL path to SERVER_NAME or an absolute URL
58
- """
59
- __tracebackhide__ = True
60
- helpers.assert_redirects(response, location, message=message)
61
-
62
- def assertStatus(self, response, status_code, message=None):
63
- __tracebackhide__ = True
64
- helpers.assert_status(response, status_code, message=message)
65
-
66
- def full_url(self, *args, **kwargs):
53
+ def assertStreamEqual(self, response1: Response, response2: Response):
67
54
  __tracebackhide__ = True
68
- return helpers.full_url(*args, **kwargs)
55
+ stream1 = list(response1.iter_encoded())
56
+ stream2 = list(response2.iter_encoded())
57
+ assert stream1 == stream2
69
58
 
70
- def login(self, user=None):
71
- self.user = self.client.login(user)
72
- return self.user
73
59
 
74
-
75
- for code in 200, 201, 204, 400, 401, 403, 404, 410, 500:
76
- name = "assert{0}".format(code)
77
- helper = getattr(helpers, name)
78
- setattr(WebTestMixin, name, lambda s, r, h=helper: h(r))
60
+ class TestCase(TestCaseMixin, unittest.TestCase):
61
+ pass
79
62
 
80
63
 
81
- @pytest.mark.usefixtures("clean_db")
82
- class DBTestMixin(object):
64
+ class PytestOnlyTestCase(TestCaseMixin):
83
65
  pass
@@ -1,12 +1,30 @@
1
1
  from contextlib import contextmanager
2
+ from urllib.parse import urlparse
2
3
 
3
4
  import pytest
4
5
 
5
- from ..frontend import FrontTestCase
6
+ from udata.mongo import db
7
+ from udata.mongo.document import get_all_models
8
+ from udata.tests import PytestOnlyTestCase, TestCase, helpers
6
9
 
7
10
 
8
11
  @pytest.mark.usefixtures("instance_path")
9
- class APITestCase(FrontTestCase):
12
+ class APITestCaseMixin:
13
+ """
14
+ See explanation about `get`, `post` overrides in :TestClientOverride
15
+
16
+ (switch from `data` in kwargs to `data` in args to avoid doing `data=data` and default to `json=True`)
17
+ """
18
+
19
+ user = None
20
+
21
+ @pytest.fixture(autouse=True)
22
+ def load_api_routes(self, app):
23
+ from udata import api, frontend
24
+
25
+ api.init_app(app)
26
+ frontend.init_app(app)
27
+
10
28
  @pytest.fixture(autouse=True)
11
29
  def inject_api(self, api):
12
30
  """
@@ -14,11 +32,22 @@ class APITestCase(FrontTestCase):
14
32
  """
15
33
  self.api = api
16
34
 
35
+ @pytest.fixture(autouse=True)
36
+ def inject_client(self, client):
37
+ """
38
+ Inject test client for compatibility with Flask-Testing.
39
+ """
40
+ self.client = client
41
+
17
42
  @contextmanager
18
43
  def api_user(self, user=None):
19
44
  with self.api.user(user) as user:
20
45
  yield user
21
46
 
47
+ def login(self, user=None):
48
+ self.user = self.client.login(user)
49
+ return self.user
50
+
22
51
  def get(self, url, *args, **kwargs):
23
52
  return self.api.get(url, *args, **kwargs)
24
53
 
@@ -36,3 +65,59 @@ class APITestCase(FrontTestCase):
36
65
 
37
66
  def options(self, url, data=None, *args, **kwargs):
38
67
  return self.api.options(url, data=data, *args, **kwargs)
68
+
69
+ def assertStatus(self, response, status_code, message=None):
70
+ __tracebackhide__ = True
71
+ helpers.assert_status(response, status_code, message=message)
72
+
73
+
74
+ for code in 200, 201, 204, 400, 401, 403, 404, 410, 500:
75
+ name = "assert{0}".format(code)
76
+ helper = getattr(helpers, name)
77
+ setattr(APITestCaseMixin, name, lambda s, r, h=helper: h(r))
78
+
79
+
80
+ class _CleanDBMixin:
81
+ """
82
+ This is only for internal use. We shouldn't inherit from this mixin but
83
+ from `DBTestCase` or `PytestOnlyDBTestCase` (or `*APITestCase`)
84
+ This is temporary while we have two hierarchies.
85
+ """
86
+
87
+ def drop_db(self, app):
88
+ """Clear the database"""
89
+ parsed_url = urlparse(app.config["MONGODB_HOST"])
90
+
91
+ # drop the leading /
92
+ db_name = parsed_url.path[1:]
93
+ db.connection.drop_database(db_name)
94
+
95
+ @pytest.fixture(autouse=True)
96
+ def _clean_db(self, app):
97
+ self.drop_db(app)
98
+ for model in get_all_models():
99
+ # When dropping the database, MongoEngine will keep the collection cached inside
100
+ # `_collection` (in memory). This cache is used to call `ensure_indexes` only on the
101
+ # first call to `_get_collection()`, on subsequent calls the value inside `_collection`
102
+ # is returned without calling `ensure_indexes`.
103
+ # In tests, the first test will have a clean memory state, so MongoEngine will initialise
104
+ # the collection and create the indexes, then the following test, with a clean database (no indexes)
105
+ # will have the collection cached, so MongoEngine will never create the indexes (except if `auto_create_index_on_save`
106
+ # is set on the model, which may be the reason it is present on most of the big models, we may remove it?)
107
+ model._collection = None
108
+
109
+
110
+ class DBTestCase(_CleanDBMixin, TestCase):
111
+ pass
112
+
113
+
114
+ class PytestOnlyDBTestCase(_CleanDBMixin, PytestOnlyTestCase):
115
+ pass
116
+
117
+
118
+ class APITestCase(APITestCaseMixin, DBTestCase):
119
+ pass
120
+
121
+
122
+ class PytestOnlyAPITestCase(APITestCaseMixin, PytestOnlyDBTestCase):
123
+ pass