udata 9.1.2.dev30355__py2.py3-none-any.whl → 9.1.2.dev30454__py2.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 (413) hide show
  1. tasks/__init__.py +109 -107
  2. tasks/helpers.py +18 -18
  3. udata/__init__.py +4 -4
  4. udata/admin/views.py +5 -5
  5. udata/api/__init__.py +111 -134
  6. udata/api/commands.py +45 -37
  7. udata/api/errors.py +5 -4
  8. udata/api/fields.py +23 -21
  9. udata/api/oauth2.py +55 -74
  10. udata/api/parsers.py +15 -15
  11. udata/api/signals.py +1 -1
  12. udata/api_fields.py +137 -89
  13. udata/app.py +58 -55
  14. udata/assets.py +5 -5
  15. udata/auth/__init__.py +37 -26
  16. udata/auth/forms.py +23 -15
  17. udata/auth/helpers.py +1 -1
  18. udata/auth/mails.py +3 -3
  19. udata/auth/password_validation.py +19 -15
  20. udata/auth/views.py +94 -68
  21. udata/commands/__init__.py +71 -69
  22. udata/commands/cache.py +7 -7
  23. udata/commands/db.py +201 -140
  24. udata/commands/dcat.py +36 -30
  25. udata/commands/fixtures.py +100 -84
  26. udata/commands/images.py +21 -20
  27. udata/commands/info.py +17 -20
  28. udata/commands/init.py +10 -10
  29. udata/commands/purge.py +12 -13
  30. udata/commands/serve.py +41 -29
  31. udata/commands/static.py +16 -18
  32. udata/commands/test.py +20 -20
  33. udata/commands/tests/fixtures.py +26 -24
  34. udata/commands/worker.py +31 -33
  35. udata/core/__init__.py +12 -12
  36. udata/core/activity/__init__.py +0 -1
  37. udata/core/activity/api.py +59 -49
  38. udata/core/activity/models.py +28 -26
  39. udata/core/activity/signals.py +1 -1
  40. udata/core/activity/tasks.py +16 -10
  41. udata/core/badges/api.py +6 -6
  42. udata/core/badges/commands.py +14 -13
  43. udata/core/badges/fields.py +8 -5
  44. udata/core/badges/forms.py +7 -4
  45. udata/core/badges/models.py +16 -31
  46. udata/core/badges/permissions.py +1 -3
  47. udata/core/badges/signals.py +2 -2
  48. udata/core/badges/tasks.py +3 -2
  49. udata/core/badges/tests/test_commands.py +10 -10
  50. udata/core/badges/tests/test_model.py +24 -31
  51. udata/core/contact_point/api.py +19 -18
  52. udata/core/contact_point/api_fields.py +21 -14
  53. udata/core/contact_point/factories.py +2 -2
  54. udata/core/contact_point/forms.py +7 -6
  55. udata/core/contact_point/models.py +3 -5
  56. udata/core/dataservices/api.py +26 -21
  57. udata/core/dataservices/factories.py +13 -11
  58. udata/core/dataservices/models.py +35 -40
  59. udata/core/dataservices/permissions.py +4 -4
  60. udata/core/dataservices/rdf.py +40 -17
  61. udata/core/dataservices/tasks.py +4 -3
  62. udata/core/dataset/actions.py +10 -10
  63. udata/core/dataset/activities.py +21 -23
  64. udata/core/dataset/api.py +321 -298
  65. udata/core/dataset/api_fields.py +443 -271
  66. udata/core/dataset/apiv2.py +305 -229
  67. udata/core/dataset/commands.py +38 -36
  68. udata/core/dataset/constants.py +61 -54
  69. udata/core/dataset/csv.py +70 -74
  70. udata/core/dataset/events.py +39 -32
  71. udata/core/dataset/exceptions.py +8 -4
  72. udata/core/dataset/factories.py +57 -65
  73. udata/core/dataset/forms.py +87 -63
  74. udata/core/dataset/models.py +336 -280
  75. udata/core/dataset/permissions.py +9 -6
  76. udata/core/dataset/preview.py +15 -17
  77. udata/core/dataset/rdf.py +156 -122
  78. udata/core/dataset/search.py +92 -77
  79. udata/core/dataset/signals.py +1 -1
  80. udata/core/dataset/tasks.py +63 -54
  81. udata/core/discussions/actions.py +5 -5
  82. udata/core/discussions/api.py +124 -120
  83. udata/core/discussions/factories.py +2 -2
  84. udata/core/discussions/forms.py +9 -7
  85. udata/core/discussions/metrics.py +1 -3
  86. udata/core/discussions/models.py +25 -24
  87. udata/core/discussions/notifications.py +18 -14
  88. udata/core/discussions/permissions.py +3 -3
  89. udata/core/discussions/signals.py +4 -4
  90. udata/core/discussions/tasks.py +24 -28
  91. udata/core/followers/api.py +32 -33
  92. udata/core/followers/models.py +9 -9
  93. udata/core/followers/signals.py +3 -3
  94. udata/core/jobs/actions.py +7 -7
  95. udata/core/jobs/api.py +99 -92
  96. udata/core/jobs/commands.py +48 -49
  97. udata/core/jobs/forms.py +11 -11
  98. udata/core/jobs/models.py +6 -6
  99. udata/core/metrics/__init__.py +2 -2
  100. udata/core/metrics/commands.py +34 -30
  101. udata/core/metrics/models.py +2 -4
  102. udata/core/metrics/signals.py +1 -1
  103. udata/core/metrics/tasks.py +3 -3
  104. udata/core/organization/activities.py +12 -15
  105. udata/core/organization/api.py +167 -174
  106. udata/core/organization/api_fields.py +183 -124
  107. udata/core/organization/apiv2.py +32 -32
  108. udata/core/organization/commands.py +20 -22
  109. udata/core/organization/constants.py +11 -11
  110. udata/core/organization/csv.py +17 -15
  111. udata/core/organization/factories.py +8 -11
  112. udata/core/organization/forms.py +32 -26
  113. udata/core/organization/metrics.py +2 -1
  114. udata/core/organization/models.py +87 -67
  115. udata/core/organization/notifications.py +18 -14
  116. udata/core/organization/permissions.py +10 -11
  117. udata/core/organization/rdf.py +14 -14
  118. udata/core/organization/search.py +30 -28
  119. udata/core/organization/signals.py +7 -7
  120. udata/core/organization/tasks.py +42 -61
  121. udata/core/owned.py +38 -27
  122. udata/core/post/api.py +82 -81
  123. udata/core/post/constants.py +8 -5
  124. udata/core/post/factories.py +4 -4
  125. udata/core/post/forms.py +13 -14
  126. udata/core/post/models.py +20 -22
  127. udata/core/post/tests/test_api.py +30 -32
  128. udata/core/reports/api.py +8 -7
  129. udata/core/reports/constants.py +1 -3
  130. udata/core/reports/models.py +10 -10
  131. udata/core/reuse/activities.py +15 -19
  132. udata/core/reuse/api.py +123 -126
  133. udata/core/reuse/api_fields.py +120 -85
  134. udata/core/reuse/apiv2.py +11 -10
  135. udata/core/reuse/constants.py +23 -23
  136. udata/core/reuse/csv.py +18 -18
  137. udata/core/reuse/factories.py +5 -9
  138. udata/core/reuse/forms.py +24 -21
  139. udata/core/reuse/models.py +55 -51
  140. udata/core/reuse/permissions.py +2 -2
  141. udata/core/reuse/search.py +49 -46
  142. udata/core/reuse/signals.py +1 -1
  143. udata/core/reuse/tasks.py +4 -5
  144. udata/core/site/api.py +47 -50
  145. udata/core/site/factories.py +2 -2
  146. udata/core/site/forms.py +4 -5
  147. udata/core/site/models.py +94 -63
  148. udata/core/site/rdf.py +14 -14
  149. udata/core/spam/api.py +16 -9
  150. udata/core/spam/constants.py +4 -4
  151. udata/core/spam/fields.py +13 -7
  152. udata/core/spam/models.py +27 -20
  153. udata/core/spam/signals.py +1 -1
  154. udata/core/spam/tests/test_spam.py +6 -5
  155. udata/core/spatial/api.py +72 -80
  156. udata/core/spatial/api_fields.py +73 -58
  157. udata/core/spatial/commands.py +67 -64
  158. udata/core/spatial/constants.py +3 -3
  159. udata/core/spatial/factories.py +37 -54
  160. udata/core/spatial/forms.py +27 -26
  161. udata/core/spatial/geoids.py +17 -17
  162. udata/core/spatial/models.py +43 -47
  163. udata/core/spatial/tasks.py +2 -1
  164. udata/core/spatial/tests/test_api.py +115 -130
  165. udata/core/spatial/tests/test_fields.py +74 -77
  166. udata/core/spatial/tests/test_geoid.py +22 -22
  167. udata/core/spatial/tests/test_models.py +5 -7
  168. udata/core/spatial/translations.py +16 -16
  169. udata/core/storages/__init__.py +16 -18
  170. udata/core/storages/api.py +66 -64
  171. udata/core/storages/tasks.py +7 -7
  172. udata/core/storages/utils.py +15 -15
  173. udata/core/storages/views.py +5 -6
  174. udata/core/tags/api.py +17 -14
  175. udata/core/tags/csv.py +4 -4
  176. udata/core/tags/models.py +8 -5
  177. udata/core/tags/tasks.py +11 -13
  178. udata/core/tags/views.py +4 -4
  179. udata/core/topic/api.py +84 -73
  180. udata/core/topic/apiv2.py +157 -127
  181. udata/core/topic/factories.py +3 -4
  182. udata/core/topic/forms.py +12 -14
  183. udata/core/topic/models.py +14 -19
  184. udata/core/topic/parsers.py +26 -26
  185. udata/core/user/activities.py +30 -29
  186. udata/core/user/api.py +151 -152
  187. udata/core/user/api_fields.py +132 -100
  188. udata/core/user/apiv2.py +7 -7
  189. udata/core/user/commands.py +38 -38
  190. udata/core/user/factories.py +8 -9
  191. udata/core/user/forms.py +14 -11
  192. udata/core/user/metrics.py +2 -2
  193. udata/core/user/models.py +68 -69
  194. udata/core/user/permissions.py +4 -5
  195. udata/core/user/rdf.py +7 -8
  196. udata/core/user/tasks.py +2 -2
  197. udata/core/user/tests/test_user_model.py +24 -16
  198. udata/cors.py +99 -0
  199. udata/db/tasks.py +2 -1
  200. udata/entrypoints.py +35 -31
  201. udata/errors.py +2 -1
  202. udata/event/values.py +6 -6
  203. udata/factories.py +2 -2
  204. udata/features/identicon/api.py +5 -6
  205. udata/features/identicon/backends.py +48 -55
  206. udata/features/identicon/tests/test_backends.py +4 -5
  207. udata/features/notifications/__init__.py +0 -1
  208. udata/features/notifications/actions.py +9 -9
  209. udata/features/notifications/api.py +17 -13
  210. udata/features/territories/__init__.py +12 -10
  211. udata/features/territories/api.py +14 -15
  212. udata/features/territories/models.py +23 -28
  213. udata/features/transfer/actions.py +8 -11
  214. udata/features/transfer/api.py +84 -77
  215. udata/features/transfer/factories.py +2 -1
  216. udata/features/transfer/models.py +11 -12
  217. udata/features/transfer/notifications.py +19 -15
  218. udata/features/transfer/permissions.py +5 -5
  219. udata/forms/__init__.py +5 -2
  220. udata/forms/fields.py +164 -172
  221. udata/forms/validators.py +19 -22
  222. udata/forms/widgets.py +9 -13
  223. udata/frontend/__init__.py +31 -26
  224. udata/frontend/csv.py +68 -58
  225. udata/frontend/markdown.py +40 -44
  226. udata/harvest/actions.py +89 -77
  227. udata/harvest/api.py +294 -238
  228. udata/harvest/backends/__init__.py +4 -4
  229. udata/harvest/backends/base.py +128 -111
  230. udata/harvest/backends/dcat.py +80 -66
  231. udata/harvest/commands.py +56 -60
  232. udata/harvest/csv.py +8 -8
  233. udata/harvest/exceptions.py +6 -3
  234. udata/harvest/filters.py +24 -23
  235. udata/harvest/forms.py +27 -28
  236. udata/harvest/models.py +88 -80
  237. udata/harvest/notifications.py +15 -10
  238. udata/harvest/signals.py +13 -13
  239. udata/harvest/tasks.py +11 -10
  240. udata/harvest/tests/factories.py +23 -24
  241. udata/harvest/tests/test_actions.py +136 -166
  242. udata/harvest/tests/test_api.py +220 -214
  243. udata/harvest/tests/test_base_backend.py +117 -112
  244. udata/harvest/tests/test_dcat_backend.py +380 -308
  245. udata/harvest/tests/test_filters.py +33 -22
  246. udata/harvest/tests/test_models.py +11 -14
  247. udata/harvest/tests/test_notifications.py +6 -7
  248. udata/harvest/tests/test_tasks.py +7 -6
  249. udata/i18n.py +237 -78
  250. udata/linkchecker/backends.py +5 -11
  251. udata/linkchecker/checker.py +23 -22
  252. udata/linkchecker/commands.py +4 -6
  253. udata/linkchecker/models.py +6 -6
  254. udata/linkchecker/tasks.py +18 -20
  255. udata/mail.py +21 -21
  256. udata/migrations/2020-07-24-remove-s-from-scope-oauth.py +9 -8
  257. udata/migrations/2020-08-24-add-fs-filename.py +9 -8
  258. udata/migrations/2020-09-28-update-reuses-datasets-metrics.py +5 -4
  259. udata/migrations/2020-10-16-migrate-ods-resources.py +9 -10
  260. udata/migrations/2021-04-08-update-schema-with-new-structure.py +8 -7
  261. udata/migrations/2021-05-27-fix-default-schema-name.py +7 -6
  262. udata/migrations/2021-07-05-remove-unused-badges.py +17 -15
  263. udata/migrations/2021-07-07-update-schema-for-community-resources.py +7 -6
  264. udata/migrations/2021-08-17-follow-integrity.py +5 -4
  265. udata/migrations/2021-08-17-harvest-integrity.py +13 -12
  266. udata/migrations/2021-08-17-oauth2client-integrity.py +5 -4
  267. udata/migrations/2021-08-17-transfer-integrity.py +5 -4
  268. udata/migrations/2021-08-17-users-integrity.py +9 -8
  269. udata/migrations/2021-12-14-reuse-topics.py +7 -6
  270. udata/migrations/2022-04-21-improve-extension-detection.py +8 -7
  271. udata/migrations/2022-09-22-clean-inactive-harvest-datasets.py +16 -14
  272. udata/migrations/2022-10-10-add-fs_uniquifier-to-user-model.py +6 -6
  273. udata/migrations/2022-10-10-migrate-harvest-extras.py +36 -26
  274. udata/migrations/2023-02-08-rename-internal-dates.py +46 -28
  275. udata/migrations/2024-01-29-fix-reuse-and-dataset-with-private-None.py +10 -8
  276. udata/migrations/2024-03-22-migrate-activity-kwargs-to-extras.py +6 -4
  277. udata/migrations/2024-06-11-fix-reuse-datasets-references.py +7 -6
  278. udata/migrations/__init__.py +123 -105
  279. udata/models/__init__.py +4 -4
  280. udata/mongo/__init__.py +13 -11
  281. udata/mongo/badges_field.py +3 -2
  282. udata/mongo/datetime_fields.py +13 -12
  283. udata/mongo/document.py +17 -16
  284. udata/mongo/engine.py +15 -16
  285. udata/mongo/errors.py +2 -1
  286. udata/mongo/extras_fields.py +30 -20
  287. udata/mongo/queryset.py +12 -12
  288. udata/mongo/slug_fields.py +38 -28
  289. udata/mongo/taglist_field.py +1 -2
  290. udata/mongo/url_field.py +5 -5
  291. udata/mongo/uuid_fields.py +4 -3
  292. udata/notifications/__init__.py +1 -1
  293. udata/notifications/mattermost.py +10 -9
  294. udata/rdf.py +167 -188
  295. udata/routing.py +40 -45
  296. udata/search/__init__.py +18 -19
  297. udata/search/adapter.py +17 -16
  298. udata/search/commands.py +44 -51
  299. udata/search/fields.py +13 -20
  300. udata/search/query.py +23 -18
  301. udata/search/result.py +9 -10
  302. udata/sentry.py +21 -19
  303. udata/settings.py +262 -198
  304. udata/sitemap.py +8 -6
  305. udata/storage/s3.py +20 -13
  306. udata/tags.py +4 -5
  307. udata/tasks.py +43 -42
  308. udata/tests/__init__.py +9 -6
  309. udata/tests/api/__init__.py +8 -6
  310. udata/tests/api/test_auth_api.py +395 -321
  311. udata/tests/api/test_base_api.py +33 -35
  312. udata/tests/api/test_contact_points.py +7 -9
  313. udata/tests/api/test_dataservices_api.py +211 -158
  314. udata/tests/api/test_datasets_api.py +823 -812
  315. udata/tests/api/test_follow_api.py +13 -15
  316. udata/tests/api/test_me_api.py +95 -112
  317. udata/tests/api/test_organizations_api.py +301 -339
  318. udata/tests/api/test_reports_api.py +35 -25
  319. udata/tests/api/test_reuses_api.py +134 -139
  320. udata/tests/api/test_swagger.py +5 -5
  321. udata/tests/api/test_tags_api.py +18 -25
  322. udata/tests/api/test_topics_api.py +94 -94
  323. udata/tests/api/test_transfer_api.py +53 -48
  324. udata/tests/api/test_user_api.py +128 -141
  325. udata/tests/apiv2/test_datasets.py +290 -198
  326. udata/tests/apiv2/test_me_api.py +10 -11
  327. udata/tests/apiv2/test_organizations.py +56 -74
  328. udata/tests/apiv2/test_swagger.py +5 -5
  329. udata/tests/apiv2/test_topics.py +69 -87
  330. udata/tests/cli/test_cli_base.py +8 -8
  331. udata/tests/cli/test_db_cli.py +21 -19
  332. udata/tests/dataservice/test_dataservice_tasks.py +8 -12
  333. udata/tests/dataset/test_csv_adapter.py +44 -35
  334. udata/tests/dataset/test_dataset_actions.py +2 -3
  335. udata/tests/dataset/test_dataset_commands.py +7 -8
  336. udata/tests/dataset/test_dataset_events.py +36 -29
  337. udata/tests/dataset/test_dataset_model.py +224 -217
  338. udata/tests/dataset/test_dataset_rdf.py +142 -131
  339. udata/tests/dataset/test_dataset_tasks.py +15 -15
  340. udata/tests/dataset/test_resource_preview.py +10 -13
  341. udata/tests/features/territories/__init__.py +9 -13
  342. udata/tests/features/territories/test_territories_api.py +71 -91
  343. udata/tests/forms/test_basic_fields.py +7 -7
  344. udata/tests/forms/test_current_user_field.py +39 -66
  345. udata/tests/forms/test_daterange_field.py +31 -39
  346. udata/tests/forms/test_dict_field.py +28 -26
  347. udata/tests/forms/test_extras_fields.py +102 -76
  348. udata/tests/forms/test_form_field.py +8 -8
  349. udata/tests/forms/test_image_field.py +33 -26
  350. udata/tests/forms/test_model_field.py +134 -123
  351. udata/tests/forms/test_model_list_field.py +7 -7
  352. udata/tests/forms/test_nested_model_list_field.py +117 -79
  353. udata/tests/forms/test_publish_as_field.py +36 -65
  354. udata/tests/forms/test_reference_field.py +34 -53
  355. udata/tests/forms/test_user_forms.py +23 -21
  356. udata/tests/forms/test_uuid_field.py +6 -10
  357. udata/tests/frontend/__init__.py +9 -6
  358. udata/tests/frontend/test_auth.py +7 -6
  359. udata/tests/frontend/test_csv.py +81 -96
  360. udata/tests/frontend/test_hooks.py +43 -43
  361. udata/tests/frontend/test_markdown.py +211 -191
  362. udata/tests/helpers.py +32 -37
  363. udata/tests/models.py +2 -2
  364. udata/tests/organization/test_csv_adapter.py +21 -16
  365. udata/tests/organization/test_notifications.py +11 -18
  366. udata/tests/organization/test_organization_model.py +13 -13
  367. udata/tests/organization/test_organization_rdf.py +29 -22
  368. udata/tests/organization/test_organization_tasks.py +16 -17
  369. udata/tests/plugin.py +79 -73
  370. udata/tests/reuse/test_reuse_model.py +21 -21
  371. udata/tests/reuse/test_reuse_task.py +11 -13
  372. udata/tests/search/__init__.py +11 -12
  373. udata/tests/search/test_adapter.py +60 -70
  374. udata/tests/search/test_query.py +16 -16
  375. udata/tests/search/test_results.py +10 -7
  376. udata/tests/site/test_site_api.py +11 -16
  377. udata/tests/site/test_site_metrics.py +20 -30
  378. udata/tests/site/test_site_model.py +4 -5
  379. udata/tests/site/test_site_rdf.py +94 -78
  380. udata/tests/test_activity.py +17 -17
  381. udata/tests/test_cors.py +62 -0
  382. udata/tests/test_discussions.py +292 -299
  383. udata/tests/test_i18n.py +37 -40
  384. udata/tests/test_linkchecker.py +91 -85
  385. udata/tests/test_mail.py +13 -17
  386. udata/tests/test_migrations.py +219 -180
  387. udata/tests/test_model.py +164 -157
  388. udata/tests/test_notifications.py +17 -17
  389. udata/tests/test_owned.py +14 -14
  390. udata/tests/test_rdf.py +25 -23
  391. udata/tests/test_routing.py +89 -93
  392. udata/tests/test_storages.py +137 -128
  393. udata/tests/test_tags.py +44 -46
  394. udata/tests/test_topics.py +7 -7
  395. udata/tests/test_transfer.py +42 -49
  396. udata/tests/test_uris.py +160 -161
  397. udata/tests/test_utils.py +79 -71
  398. udata/tests/user/test_user_rdf.py +5 -9
  399. udata/tests/workers/test_jobs_commands.py +57 -58
  400. udata/tests/workers/test_tasks_routing.py +23 -29
  401. udata/tests/workers/test_workers_api.py +125 -131
  402. udata/tests/workers/test_workers_helpers.py +6 -6
  403. udata/tracking.py +4 -6
  404. udata/uris.py +45 -46
  405. udata/utils.py +68 -66
  406. udata/wsgi.py +1 -1
  407. {udata-9.1.2.dev30355.dist-info → udata-9.1.2.dev30454.dist-info}/METADATA +7 -3
  408. udata-9.1.2.dev30454.dist-info/RECORD +706 -0
  409. udata-9.1.2.dev30355.dist-info/RECORD +0 -704
  410. {udata-9.1.2.dev30355.dist-info → udata-9.1.2.dev30454.dist-info}/LICENSE +0 -0
  411. {udata-9.1.2.dev30355.dist-info → udata-9.1.2.dev30454.dist-info}/WHEEL +0 -0
  412. {udata-9.1.2.dev30355.dist-info → udata-9.1.2.dev30454.dist-info}/entry_points.txt +0 -0
  413. {udata-9.1.2.dev30355.dist-info → udata-9.1.2.dev30454.dist-info}/top_level.txt +0 -0
udata/core/user/models.py CHANGED
@@ -1,34 +1,33 @@
1
+ import json
1
2
  from copy import copy
2
3
  from datetime import datetime
3
4
  from itertools import chain
4
- import json
5
5
  from time import time
6
6
 
7
7
  from authlib.jose import JsonWebSignature
8
8
  from blinker import Signal
9
9
  from flask import current_app
10
- from flask_security import UserMixin, RoleMixin, MongoEngineUserDatastore
11
- from mongoengine.signals import pre_save, post_save
12
-
10
+ from flask_security import MongoEngineUserDatastore, RoleMixin, UserMixin
11
+ from mongoengine.signals import post_save, pre_save
13
12
  from werkzeug.utils import cached_property
14
13
 
15
14
  from udata import mail
16
15
  from udata.core import storages
17
- from udata.uris import endpoint_for
18
- from udata.frontend.markdown import mdstrip
19
- from udata.i18n import lazy_gettext as _
20
- from udata.models import db, WithMetrics, Follow
21
16
  from udata.core.discussions.models import Discussion
22
17
  from udata.core.storages import avatars, default_image_basename
23
- from .constants import AVATAR_SIZES
18
+ from udata.frontend.markdown import mdstrip
19
+ from udata.i18n import lazy_gettext as _
20
+ from udata.models import Follow, WithMetrics, db
21
+ from udata.uris import endpoint_for
24
22
 
25
- __all__ = ('User', 'Role', 'datastore')
23
+ from .constants import AVATAR_SIZES
26
24
 
25
+ __all__ = ("User", "Role", "datastore")
27
26
 
28
27
 
29
28
  # TODO: use simple text for role
30
29
  class Role(db.Document, RoleMixin):
31
- ADMIN = 'admin'
30
+ ADMIN = "admin"
32
31
  name = db.StringField(max_length=80, unique=True)
33
32
  description = db.StringField(max_length=255)
34
33
  permissions = db.ListField()
@@ -42,8 +41,7 @@ class UserSettings(db.EmbeddedDocument):
42
41
 
43
42
 
44
43
  class User(WithMetrics, UserMixin, db.Document):
45
- slug = db.SlugField(
46
- max_length=255, required=True, populate_from='fullname')
44
+ slug = db.SlugField(max_length=255, required=True, populate_from="fullname")
47
45
  email = db.StringField(max_length=255, required=True, unique=True)
48
46
  password = db.StringField()
49
47
  active = db.BooleanField()
@@ -54,8 +52,7 @@ class User(WithMetrics, UserMixin, db.Document):
54
52
  last_name = db.StringField(max_length=255, required=True)
55
53
 
56
54
  avatar_url = db.URLField()
57
- avatar = db.ImageField(
58
- fs=avatars, basename=default_image_basename, thumbnails=AVATAR_SIZES)
55
+ avatar = db.ImageField(fs=avatars, basename=default_image_basename, thumbnails=AVATAR_SIZES)
59
56
  website = db.URLField()
60
57
  about = db.StringField()
61
58
 
@@ -93,16 +90,16 @@ class User(WithMetrics, UserMixin, db.Document):
93
90
  on_delete = Signal()
94
91
 
95
92
  meta = {
96
- 'indexes': ['$slug', '-created_at', 'slug', 'apikey'],
97
- 'ordering': ['-created_at'],
98
- 'auto_create_index_on_save': True
93
+ "indexes": ["$slug", "-created_at", "slug", "apikey"],
94
+ "ordering": ["-created_at"],
95
+ "auto_create_index_on_save": True,
99
96
  }
100
97
 
101
98
  __metrics_keys__ = [
102
- 'datasets',
103
- 'reuses',
104
- 'following',
105
- 'followers',
99
+ "datasets",
100
+ "reuses",
101
+ "following",
102
+ "followers",
106
103
  ]
107
104
 
108
105
  def __str__(self):
@@ -110,19 +107,20 @@ class User(WithMetrics, UserMixin, db.Document):
110
107
 
111
108
  @property
112
109
  def fullname(self):
113
- return ' '.join((self.first_name or '', self.last_name or '')).strip()
110
+ return " ".join((self.first_name or "", self.last_name or "")).strip()
114
111
 
115
112
  @cached_property
116
113
  def organizations(self):
117
114
  from udata.core.organization.models import Organization
115
+
118
116
  return Organization.objects(members__user=self, deleted__exists=False)
119
117
 
120
118
  @property
121
119
  def sysadmin(self):
122
- return self.has_role('admin')
120
+ return self.has_role("admin")
123
121
 
124
122
  def url_for(self, *args, **kwargs):
125
- return endpoint_for('users.show', 'api.user', user=self, *args, **kwargs)
123
+ return endpoint_for("users.show", "api.user", user=self, *args, **kwargs)
126
124
 
127
125
  display_url = property(url_for)
128
126
 
@@ -132,23 +130,19 @@ class User(WithMetrics, UserMixin, db.Document):
132
130
 
133
131
  @property
134
132
  def visible(self):
135
- count = self.metrics.get('datasets', 0) + self.metrics.get('reuses', 0)
133
+ count = self.metrics.get("datasets", 0) + self.metrics.get("reuses", 0)
136
134
  return count > 0 and self.active
137
135
 
138
136
  @cached_property
139
137
  def resources_availability(self):
140
138
  """Return the percentage of availability for resources."""
141
139
  # Flatten the list.
142
- availabilities = list(
143
- chain(
144
- *[org.check_availability() for org in self.organizations]
145
- )
146
- )
140
+ availabilities = list(chain(*[org.check_availability() for org in self.organizations]))
147
141
  # Filter out the unknown
148
142
  availabilities = [a for a in availabilities if type(a) is bool]
149
143
  if availabilities:
150
144
  # Trick will work because it's a sum() of booleans.
151
- return round(100. * sum(availabilities) / len(availabilities), 2)
145
+ return round(100.0 * sum(availabilities) / len(availabilities), 2)
152
146
  # if nothing is unavailable, everything is considered OK
153
147
  return 100
154
148
 
@@ -156,35 +150,38 @@ class User(WithMetrics, UserMixin, db.Document):
156
150
  def datasets_org_count(self):
157
151
  """Return the number of datasets of user's organizations."""
158
152
  from udata.models import Dataset # Circular imports.
159
- return sum(Dataset.objects(organization=org).visible().count()
160
- for org in self.organizations)
153
+
154
+ return sum(
155
+ Dataset.objects(organization=org).visible().count() for org in self.organizations
156
+ )
161
157
 
162
158
  @cached_property
163
159
  def followers_org_count(self):
164
160
  """Return the number of followers of user's organizations."""
165
161
  from udata.models import Follow # Circular imports.
166
- return sum(Follow.objects(following=org).count()
167
- for org in self.organizations)
162
+
163
+ return sum(Follow.objects(following=org).count() for org in self.organizations)
168
164
 
169
165
  @property
170
166
  def datasets_count(self):
171
167
  """Return the number of datasets of the user."""
172
- return self.metrics.get('datasets', 0)
168
+ return self.metrics.get("datasets", 0)
173
169
 
174
170
  @property
175
171
  def followers_count(self):
176
172
  """Return the number of followers of the user."""
177
- return self.metrics.get('followers', 0)
173
+ return self.metrics.get("followers", 0)
178
174
 
179
175
  def generate_api_key(self):
180
176
  payload = {
181
- 'user': str(self.id),
182
- 'time': time(),
177
+ "user": str(self.id),
178
+ "time": time(),
183
179
  }
184
- s = JsonWebSignature(algorithms=['HS512']).serialize_compact(
185
- {'alg': 'HS512'},
186
- json.dumps(payload, separators=(',', ':')),
187
- current_app.config['SECRET_KEY'])
180
+ s = JsonWebSignature(algorithms=["HS512"]).serialize_compact(
181
+ {"alg": "HS512"},
182
+ json.dumps(payload, separators=(",", ":")),
183
+ current_app.config["SECRET_KEY"],
184
+ )
188
185
  self.apikey = s.decode()
189
186
 
190
187
  def clear_api_key(self):
@@ -202,28 +199,27 @@ class User(WithMetrics, UserMixin, db.Document):
202
199
  @classmethod
203
200
  def post_save(cls, sender, document, **kwargs):
204
201
  cls.after_save.send(document)
205
- if kwargs.get('created'):
202
+ if kwargs.get("created"):
206
203
  cls.on_create.send(document)
207
204
  else:
208
205
  cls.on_update.send(document)
209
206
 
210
207
  @cached_property
211
208
  def json_ld(self):
212
-
213
209
  result = {
214
- '@type': 'Person',
215
- '@context': 'http://schema.org',
216
- 'name': self.fullname,
210
+ "@type": "Person",
211
+ "@context": "http://schema.org",
212
+ "name": self.fullname,
217
213
  }
218
214
 
219
215
  if self.about:
220
- result['description'] = mdstrip(self.about)
216
+ result["description"] = mdstrip(self.about)
221
217
 
222
218
  if self.avatar_url:
223
- result['image'] = self.avatar_url
219
+ result["image"] = self.avatar_url
224
220
 
225
221
  if self.website:
226
- result['url'] = self.website
222
+ result["url"] = self.website
227
223
 
228
224
  return result
229
225
 
@@ -231,8 +227,8 @@ class User(WithMetrics, UserMixin, db.Document):
231
227
  return db.Document.delete(self, *args, **kwargs)
232
228
 
233
229
  def delete(self, *args, **kwargs):
234
- raise NotImplementedError('''This method should not be using directly.
235
- Use `mark_as_deleted` (or `_delete` if you know what you're doing)''')
230
+ raise NotImplementedError("""This method should not be using directly.
231
+ Use `mark_as_deleted` (or `_delete` if you know what you're doing)""")
236
232
 
237
233
  def mark_as_deleted(self, notify: bool = True):
238
234
  if self.avatar.filename is not None:
@@ -242,14 +238,13 @@ class User(WithMetrics, UserMixin, db.Document):
242
238
  for key, value in self.avatar.thumbnails.items():
243
239
  storage.delete(value)
244
240
 
245
-
246
241
  copied_user = copy(self)
247
- self.email = '{}@deleted'.format(self.id)
248
- self.slug = 'deleted'
242
+ self.email = "{}@deleted".format(self.id)
243
+ self.slug = "deleted"
249
244
  self.password = None
250
245
  self.active = False
251
- self.first_name = 'DELETED'
252
- self.last_name = 'DELETED'
246
+ self.first_name = "DELETED"
247
+ self.last_name = "DELETED"
253
248
  self.avatar = None
254
249
  self.avatar_url = None
255
250
  self.website = None
@@ -259,9 +254,9 @@ class User(WithMetrics, UserMixin, db.Document):
259
254
  self.deleted = datetime.utcnow()
260
255
  self.save()
261
256
  for organization in self.organizations:
262
- organization.members = [member
263
- for member in organization.members
264
- if member.user != self]
257
+ organization.members = [
258
+ member for member in organization.members if member.user != self
259
+ ]
265
260
  organization.save()
266
261
  for discussion in Discussion.objects(discussion__posted_by=self):
267
262
  # Remove all discussions with current user as only participant
@@ -271,36 +266,40 @@ class User(WithMetrics, UserMixin, db.Document):
271
266
 
272
267
  for message in discussion.discussion:
273
268
  if message.posted_by == self:
274
- message.content = 'DELETED'
269
+ message.content = "DELETED"
275
270
  discussion.save()
276
271
  Follow.objects(follower=self).delete()
277
272
  Follow.objects(following=self).delete()
278
273
 
279
274
  from udata.models import ContactPoint
280
- ContactPoint.objects(owner=self).delete()
281
275
 
276
+ ContactPoint.objects(owner=self).delete()
282
277
 
283
278
  if notify:
284
- mail.send(_('Account deletion'), copied_user, 'account_deleted')
279
+ mail.send(_("Account deletion"), copied_user, "account_deleted")
285
280
 
286
281
  def count_datasets(self):
287
282
  from udata.models import Dataset
288
- self.metrics['datasets'] = Dataset.objects(owner=self).visible().count()
283
+
284
+ self.metrics["datasets"] = Dataset.objects(owner=self).visible().count()
289
285
  self.save()
290
286
 
291
287
  def count_reuses(self):
292
288
  from udata.models import Reuse
293
- self.metrics['reuses'] = Reuse.objects(owner=self).visible().count()
289
+
290
+ self.metrics["reuses"] = Reuse.objects(owner=self).visible().count()
294
291
  self.save()
295
292
 
296
293
  def count_followers(self):
297
294
  from udata.models import Follow
298
- self.metrics['followers'] = Follow.objects(until=None).followers(self).count()
295
+
296
+ self.metrics["followers"] = Follow.objects(until=None).followers(self).count()
299
297
  self.save()
300
298
 
301
299
  def count_following(self):
302
300
  from udata.models import Follow
303
- self.metrics['following'] = Follow.objects.following(self).count()
301
+
302
+ self.metrics["following"] = Follow.objects.following(self).count()
304
303
  self.save()
305
304
 
306
305
 
@@ -1,14 +1,13 @@
1
1
  from udata.auth import Permission, RoleNeed, UserNeed
2
2
  from udata.i18n import lazy_gettext as _
3
3
 
4
-
5
4
  ROLES = {
6
- 'admin': _('System administrator'),
7
- 'editor': _('Site editorialist'),
8
- 'moderator': _('Site moderator'),
5
+ "admin": _("System administrator"),
6
+ "editor": _("Site editorialist"),
7
+ "moderator": _("Site moderator"),
9
8
  }
10
9
 
11
- sysadmin = Permission(RoleNeed('admin'))
10
+ sysadmin = Permission(RoleNeed("admin"))
12
11
 
13
12
 
14
13
  class UserEditPermission(Permission):
udata/core/user/rdf.py CHANGED
@@ -1,22 +1,21 @@
1
- '''
1
+ """
2
2
  This module centralize user helpers for RDF/DCAT serialization and parsing
3
- '''
3
+ """
4
4
 
5
- from rdflib import Graph, URIRef, Literal, BNode
6
- from rdflib.namespace import RDF, RDFS, FOAF
5
+ from rdflib import BNode, Graph, Literal, URIRef
6
+ from rdflib.namespace import FOAF, RDF, RDFS
7
7
 
8
8
  from udata.rdf import namespace_manager
9
9
  from udata.uris import endpoint_for
10
10
 
11
11
 
12
12
  def user_to_rdf(user, graph=None):
13
- '''
13
+ """
14
14
  Map a Resource domain model to a DCAT/RDF graph
15
- '''
15
+ """
16
16
  graph = graph or Graph(namespace_manager=namespace_manager)
17
17
  if user.id:
18
- user_url = endpoint_for('users.show_redirect', 'api.user',
19
- user=user.id, _external=True)
18
+ user_url = endpoint_for("users.show_redirect", "api.user", user=user.id, _external=True)
20
19
  id = URIRef(user_url)
21
20
  else:
22
21
  id = BNode()
udata/core/user/tasks.py CHANGED
@@ -9,7 +9,7 @@ from .models import datastore
9
9
  log = logging.getLogger(__name__)
10
10
 
11
11
 
12
- @task(route='high.mail')
12
+ @task(route="high.mail")
13
13
  def send_test_mail(email):
14
14
  user = datastore.find_user(email=email)
15
- mail.send(_('Test mail'), user, 'test')
15
+ mail.send(_("Test mail"), user, "test")
@@ -1,15 +1,13 @@
1
1
  import pytest
2
2
 
3
- from udata.core.discussions.factories import (
4
- MessageDiscussionFactory, DiscussionFactory
5
- )
3
+ from udata.core.discussions.factories import DiscussionFactory, MessageDiscussionFactory
4
+ from udata.core.discussions.models import Discussion
6
5
  from udata.core.followers.models import Follow
6
+ from udata.core.organization.factories import OrganizationFactory
7
7
  from udata.core.user.factories import UserFactory
8
8
  from udata.core.user.models import User
9
- from udata.core.discussions.models import Discussion
10
- from udata.core.organization.factories import OrganizationFactory
11
9
 
12
- pytestmark = pytest.mark.usefixtures('clean_db')
10
+ pytestmark = pytest.mark.usefixtures("clean_db")
13
11
 
14
12
 
15
13
  @pytest.mark.frontend
@@ -20,12 +18,22 @@ class UserModelTest:
20
18
  user = UserFactory()
21
19
  other_user = UserFactory()
22
20
  org = OrganizationFactory(editors=[user])
23
- discussion_only_user = DiscussionFactory(user=user, subject=org, discussion=[
24
- MessageDiscussionFactory(posted_by=user), MessageDiscussionFactory(posted_by=user)
25
- ])
26
- discussion_with_other = DiscussionFactory(user=other_user, subject=org, discussion=[
27
- MessageDiscussionFactory(posted_by=other_user), MessageDiscussionFactory(posted_by=user)
28
- ])
21
+ discussion_only_user = DiscussionFactory(
22
+ user=user,
23
+ subject=org,
24
+ discussion=[
25
+ MessageDiscussionFactory(posted_by=user),
26
+ MessageDiscussionFactory(posted_by=user),
27
+ ],
28
+ )
29
+ discussion_with_other = DiscussionFactory(
30
+ user=other_user,
31
+ subject=org,
32
+ discussion=[
33
+ MessageDiscussionFactory(posted_by=other_user),
34
+ MessageDiscussionFactory(posted_by=user),
35
+ ],
36
+ )
29
37
  user_follow_org = Follow.objects.create(follower=user, following=org)
30
38
  user_followed = Follow.objects.create(follower=other_user, following=user)
31
39
 
@@ -36,12 +44,12 @@ class UserModelTest:
36
44
 
37
45
  assert Discussion.objects(id=discussion_only_user.id).first() is None
38
46
  discussion_with_other.reload()
39
- assert discussion_with_other.discussion[1].content == 'DELETED'
47
+ assert discussion_with_other.discussion[1].content == "DELETED"
40
48
 
41
49
  assert Follow.objects(id=user_follow_org.id).first() is None
42
50
  assert Follow.objects(id=user_followed.id).first() is None
43
51
 
44
- assert user.slug == 'deleted'
52
+ assert user.slug == "deleted"
45
53
 
46
54
  def test_mark_as_deleted_slug_multiple(self):
47
55
  user = UserFactory()
@@ -50,8 +58,8 @@ class UserModelTest:
50
58
  user.mark_as_deleted()
51
59
  other_user.mark_as_deleted()
52
60
 
53
- assert user.slug == 'deleted'
54
- assert other_user.slug == 'deleted-1'
61
+ assert user.slug == "deleted"
62
+ assert other_user.slug == "deleted-1"
55
63
 
56
64
  def test_delete_safeguard(self):
57
65
  user = UserFactory()
udata/cors.py ADDED
@@ -0,0 +1,99 @@
1
+ import logging
2
+
3
+ from flask import request
4
+ from werkzeug.datastructures import Headers
5
+
6
+ log = logging.getLogger(__name__)
7
+
8
+
9
+ def add_vary(headers: Headers, header: str):
10
+ values = headers.getlist("Vary")
11
+ if header not in values:
12
+ values.append(header)
13
+ headers.set("Vary", ", ".join(values))
14
+
15
+
16
+ def add_actual_request_headers(headers: Headers) -> Headers:
17
+ origin = request.headers.get("Origin", None)
18
+ if origin:
19
+ headers.set("Access-Control-Allow-Origin", origin)
20
+ add_vary(headers, "Origin")
21
+
22
+ headers.set("Access-Control-Allow-Credentials", "true")
23
+
24
+ return headers
25
+
26
+
27
+ def is_preflight_request() -> bool:
28
+ return (
29
+ request.method == "OPTIONS"
30
+ and request.headers.get("Access-Control-Request-Method", None) is not None
31
+ )
32
+
33
+
34
+ def is_allowed_cors_route():
35
+ return (
36
+ request.path.endswith((".js", ".css", ".woff", ".woff2", ".png", ".jpg", ".jpeg", ".svg"))
37
+ or request.path.startswith("/api")
38
+ or request.path.startswith("/oauth")
39
+ )
40
+
41
+
42
+ def add_preflight_request_headers(headers: Headers) -> Headers:
43
+ origin = request.headers.get("Origin", None)
44
+ if origin:
45
+ headers.set("Access-Control-Allow-Origin", origin)
46
+ add_vary(headers, "Origin")
47
+
48
+ headers.set("Access-Control-Allow-Credentials", "true")
49
+
50
+ # The API allows all methods, so just copy the browser requested methods from the request headers.
51
+ headers.set(
52
+ "Access-Control-Allow-Methods", request.headers.get("Access-Control-Request-Method", "")
53
+ )
54
+ add_vary(headers, "Access-Control-Request-Method")
55
+
56
+ headers.set(
57
+ "Access-Control-Allow-Headers",
58
+ request.headers.get("Access-Control-Request-Headers", ""),
59
+ )
60
+ add_vary(headers, "Access-Control-Request-Headers")
61
+
62
+ return headers
63
+
64
+
65
+ def init_app(app):
66
+ """
67
+ The CORS should be enabled before routing to trigger before some redirects.
68
+
69
+ For OPTIONS requests, we do not call the backend API code at all.
70
+ We just return the access-control headers.
71
+
72
+ For other requests, we append the access-control headers to the original
73
+ response.
74
+
75
+ This module is inspired by:
76
+ - the CORS logic https://github.com/fruitcake/php-cors/blob/master/src/CorsService.php
77
+ - the middleware https://github.com/laravel/framework/blob/11.x/src/Illuminate/Http/Middleware/HandleCors.php
78
+ """
79
+
80
+ @app.before_request
81
+ def bypass_code_for_options_requests():
82
+ if not is_allowed_cors_route():
83
+ return
84
+
85
+ if is_preflight_request():
86
+ headers = add_preflight_request_headers(Headers())
87
+ return "", 204, headers
88
+
89
+ @app.after_request
90
+ def add_cors_headers(response):
91
+ if not is_allowed_cors_route():
92
+ return response
93
+
94
+ if request.method == "OPTIONS":
95
+ add_vary(response.headers, "Access-Control-Request-Method")
96
+
97
+ add_actual_request_headers(response.headers)
98
+
99
+ return response
udata/db/tasks.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from udata.commands.db import check_references
2
2
  from udata.tasks import job
3
3
 
4
- @job('check-integrity')
4
+
5
+ @job("check-integrity")
5
6
  def check_integrity(self):
6
7
  check_references()