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
@@ -1,127 +1,130 @@
1
- from bson import ObjectId
2
1
  from datetime import datetime
3
2
 
4
- from flask_security import current_user
3
+ from bson import ObjectId
5
4
  from flask_restx.inputs import boolean
5
+ from flask_security import current_user
6
6
 
7
+ from udata.api import API, api, fields
7
8
  from udata.auth import admin_permission
8
- from udata.api import api, API, fields
9
9
  from udata.core.spam.api import SpamAPIMixin
10
10
  from udata.core.spam.fields import spam_fields
11
- from udata.utils import id_or_404
12
11
  from udata.core.user.api_fields import user_ref_fields
12
+ from udata.utils import id_or_404
13
13
 
14
- from .forms import DiscussionCreateForm, DiscussionCommentForm
15
- from .models import Message, Discussion
14
+ from .forms import DiscussionCommentForm, DiscussionCreateForm
15
+ from .models import Discussion, Message
16
16
  from .permissions import CloseDiscussionPermission
17
17
  from .signals import on_discussion_deleted
18
18
 
19
-
20
- ns = api.namespace('discussions', 'Discussion related operations')
21
-
22
- message_fields = api.model('DiscussionMessage', {
23
- 'content': fields.String(description='The message body'),
24
- 'posted_by': fields.Nested(user_ref_fields,
25
- description='The message author'),
26
- 'posted_on': fields.ISODateTime(description='The message posting date'),
27
- 'spam': fields.Nested(spam_fields),
28
- })
29
-
30
- discussion_fields = api.model('Discussion', {
31
- 'id': fields.String(description='The discussion identifier'),
32
- 'subject': fields.Nested(api.model_reference,
33
- description='The discussion target object'),
34
- 'class': fields.ClassName(description='The object class',
35
- discriminator=True),
36
- 'title': fields.String(description='The discussion title'),
37
- 'user': fields.Nested(
38
- user_ref_fields, description='The discussion author'),
39
- 'created': fields.ISODateTime(description='The discussion creation date'),
40
- 'closed': fields.ISODateTime(description='The discussion closing date'),
41
- 'closed_by': fields.Nested(user_ref_fields, allow_null=True,
42
- description='The user who closed the discussion'),
43
- 'discussion': fields.Nested(message_fields),
44
- 'url': fields.UrlFor('api.discussion',
45
- description='The discussion API URI'),
46
- 'extras': fields.Raw(description='Extra attributes as key-value pairs'),
47
- 'spam': fields.Nested(spam_fields),
48
- })
49
-
50
- start_discussion_fields = api.model('DiscussionStart', {
51
- 'title': fields.String(description='The title of the discussion to open',
52
- required=True),
53
- 'comment': fields.String(description='The content of the initial comment',
54
- required=True),
55
- 'subject': fields.Nested(api.model_reference,
56
- description='The discussion target object',
57
- required=True),
58
- 'extras': fields.Raw(description='Extras attributes as key-value pairs'),
59
- })
60
-
61
- comment_discussion_fields = api.model('DiscussionResponse', {
62
- 'comment': fields.String(
63
- description='The comment to submit', required=True),
64
- 'close': fields.Boolean(
65
- description='Is this a closing response. Only subject owner can close')
66
- })
67
-
68
- discussion_page_fields = api.model('DiscussionPage',
69
- fields.pager(discussion_fields))
19
+ ns = api.namespace("discussions", "Discussion related operations")
20
+
21
+ message_fields = api.model(
22
+ "DiscussionMessage",
23
+ {
24
+ "content": fields.String(description="The message body"),
25
+ "posted_by": fields.Nested(user_ref_fields, description="The message author"),
26
+ "posted_on": fields.ISODateTime(description="The message posting date"),
27
+ "spam": fields.Nested(spam_fields),
28
+ },
29
+ )
30
+
31
+ discussion_fields = api.model(
32
+ "Discussion",
33
+ {
34
+ "id": fields.String(description="The discussion identifier"),
35
+ "subject": fields.Nested(api.model_reference, description="The discussion target object"),
36
+ "class": fields.ClassName(description="The object class", discriminator=True),
37
+ "title": fields.String(description="The discussion title"),
38
+ "user": fields.Nested(user_ref_fields, description="The discussion author"),
39
+ "created": fields.ISODateTime(description="The discussion creation date"),
40
+ "closed": fields.ISODateTime(description="The discussion closing date"),
41
+ "closed_by": fields.Nested(
42
+ user_ref_fields, allow_null=True, description="The user who closed the discussion"
43
+ ),
44
+ "discussion": fields.Nested(message_fields),
45
+ "url": fields.UrlFor("api.discussion", description="The discussion API URI"),
46
+ "extras": fields.Raw(description="Extra attributes as key-value pairs"),
47
+ "spam": fields.Nested(spam_fields),
48
+ },
49
+ )
50
+
51
+ start_discussion_fields = api.model(
52
+ "DiscussionStart",
53
+ {
54
+ "title": fields.String(description="The title of the discussion to open", required=True),
55
+ "comment": fields.String(description="The content of the initial comment", required=True),
56
+ "subject": fields.Nested(
57
+ api.model_reference, description="The discussion target object", required=True
58
+ ),
59
+ "extras": fields.Raw(description="Extras attributes as key-value pairs"),
60
+ },
61
+ )
62
+
63
+ comment_discussion_fields = api.model(
64
+ "DiscussionResponse",
65
+ {
66
+ "comment": fields.String(description="The comment to submit", required=True),
67
+ "close": fields.Boolean(
68
+ description="Is this a closing response. Only subject owner can close"
69
+ ),
70
+ },
71
+ )
72
+
73
+ discussion_page_fields = api.model("DiscussionPage", fields.pager(discussion_fields))
70
74
 
71
75
  parser = api.parser()
72
76
  parser.add_argument(
73
- 'sort', type=str, default='-created', location='args',
74
- help='The sorting attribute')
77
+ "sort", type=str, default="-created", location="args", help="The sorting attribute"
78
+ )
75
79
  parser.add_argument(
76
- 'closed', type=boolean, location='args',
77
- help='Filters discussions on their closed status if specified')
80
+ "closed",
81
+ type=boolean,
82
+ location="args",
83
+ help="Filters discussions on their closed status if specified",
84
+ )
78
85
  parser.add_argument(
79
- 'for', type=str, location='args', action='append',
80
- help='Filter discussions for a given subject')
86
+ "for", type=str, location="args", action="append", help="Filter discussions for a given subject"
87
+ )
88
+ parser.add_argument("user", type=str, location="args", help="Filter discussions created by a user")
89
+ parser.add_argument("page", type=int, default=1, location="args", help="The page to fetch")
81
90
  parser.add_argument(
82
- 'user', type=str, location='args',
83
- help='Filter discussions created by a user')
84
- parser.add_argument(
85
- 'page', type=int, default=1, location='args', help='The page to fetch')
86
- parser.add_argument(
87
- 'page_size', type=int, default=20, location='args',
88
- help='The page size to fetch')
91
+ "page_size", type=int, default=20, location="args", help="The page size to fetch"
92
+ )
89
93
 
90
94
 
91
- @ns.route('/<id>/spam/', endpoint='discussion_spam')
92
- @ns.doc(delete={'id': 'unspam'})
95
+ @ns.route("/<id>/spam/", endpoint="discussion_spam")
96
+ @ns.doc(delete={"id": "unspam"})
93
97
  class DiscussionSpamAPI(SpamAPIMixin):
94
98
  model = Discussion
95
99
 
96
100
 
97
- @ns.route('/<id>/', endpoint='discussion')
101
+ @ns.route("/<id>/", endpoint="discussion")
98
102
  class DiscussionAPI(API):
99
- '''
103
+ """
100
104
  Base class for a discussion thread.
101
- '''
102
- @api.doc('get_discussion')
105
+ """
106
+
107
+ @api.doc("get_discussion")
103
108
  @api.marshal_with(discussion_fields)
104
109
  def get(self, id):
105
- '''Get a discussion given its ID'''
110
+ """Get a discussion given its ID"""
106
111
  discussion = Discussion.objects.get_or_404(id=id_or_404(id))
107
112
  return discussion
108
113
 
109
114
  @api.secure
110
- @api.doc('comment_discussion')
115
+ @api.doc("comment_discussion")
111
116
  @api.expect(comment_discussion_fields)
112
- @api.response(403, 'Not allowed to close this discussion '
113
- "OR can't add comments on a closed discussion")
117
+ @api.response(
118
+ 403, "Not allowed to close this discussion " "OR can't add comments on a closed discussion"
119
+ )
114
120
  @api.marshal_with(discussion_fields)
115
121
  def post(self, id):
116
- '''Add comment and optionally close a discussion given its ID'''
122
+ """Add comment and optionally close a discussion given its ID"""
117
123
  discussion = Discussion.objects.get_or_404(id=id_or_404(id))
118
124
  if discussion.closed:
119
125
  api.abort(403, "Can't add comments on a closed discussion")
120
126
  form = api.validate(DiscussionCommentForm)
121
- message = Message(
122
- content=form.comment.data,
123
- posted_by=current_user.id
124
- )
127
+ message = Message(content=form.comment.data, posted_by=current_user.id)
125
128
  discussion.discussion.append(message)
126
129
  message_idx = len(discussion.discussion) - 1
127
130
  close = form.close.data
@@ -137,81 +140,82 @@ class DiscussionAPI(API):
137
140
  return discussion
138
141
 
139
142
  @api.secure(admin_permission)
140
- @api.doc('delete_discussion')
141
- @api.response(403, 'Not allowed to delete this discussion')
143
+ @api.doc("delete_discussion")
144
+ @api.response(403, "Not allowed to delete this discussion")
142
145
  def delete(self, id):
143
- '''Delete a discussion given its ID'''
146
+ """Delete a discussion given its ID"""
144
147
  discussion = Discussion.objects.get_or_404(id=id_or_404(id))
145
148
  discussion.delete()
146
149
  on_discussion_deleted.send(discussion)
147
- return '', 204
150
+ return "", 204
148
151
 
149
152
 
150
- @ns.route('/<id>/comments/<int:cidx>/spam', endpoint='discussion_comment_spam')
151
- @ns.doc(delete={'id': 'unspam'})
153
+ @ns.route("/<id>/comments/<int:cidx>/spam", endpoint="discussion_comment_spam")
154
+ @ns.doc(delete={"id": "unspam"})
152
155
  class DiscussionSpamAPI(SpamAPIMixin):
153
156
  def get_model(self, id, cidx):
154
157
  discussion = Discussion.objects.get_or_404(id=id_or_404(id))
155
158
  if len(discussion.discussion) <= cidx:
156
- api.abort(404, 'Comment does not exist')
159
+ api.abort(404, "Comment does not exist")
157
160
  elif cidx == 0:
158
- api.abort(400, 'You cannot unspam the first comment of a discussion')
161
+ api.abort(400, "You cannot unspam the first comment of a discussion")
159
162
  return discussion, discussion.discussion[cidx]
160
163
 
161
- @ns.route('/<id>/comments/<int:cidx>', endpoint='discussion_comment')
164
+
165
+ @ns.route("/<id>/comments/<int:cidx>", endpoint="discussion_comment")
162
166
  class DiscussionCommentAPI(API):
163
- '''
167
+ """
164
168
  Base class for a comment in a discussion thread.
165
- '''
169
+ """
170
+
166
171
  @api.secure(admin_permission)
167
- @api.doc('delete_discussion_comment')
168
- @api.response(403, 'Not allowed to delete this comment')
172
+ @api.doc("delete_discussion_comment")
173
+ @api.response(403, "Not allowed to delete this comment")
169
174
  def delete(self, id, cidx):
170
- '''Delete a comment given its index'''
175
+ """Delete a comment given its index"""
171
176
  discussion = Discussion.objects.get_or_404(id=id_or_404(id))
172
177
  if len(discussion.discussion) <= cidx:
173
- api.abort(404, 'Comment does not exist')
178
+ api.abort(404, "Comment does not exist")
174
179
  elif cidx == 0:
175
- api.abort(400, 'You cannot delete the first comment of a discussion')
180
+ api.abort(400, "You cannot delete the first comment of a discussion")
176
181
  discussion.discussion.pop(cidx)
177
182
  discussion.save()
178
- return '', 204
183
+ return "", 204
179
184
 
180
185
 
181
- @ns.route('/', endpoint='discussions')
186
+ @ns.route("/", endpoint="discussions")
182
187
  class DiscussionsAPI(API):
183
- '''
188
+ """
184
189
  Base class for a list of discussions.
185
- '''
186
- @api.doc('list_discussions')
190
+ """
191
+
192
+ @api.doc("list_discussions")
187
193
  @api.expect(parser)
188
194
  @api.marshal_with(discussion_page_fields)
189
195
  def get(self):
190
- '''List all Discussions'''
196
+ """List all Discussions"""
191
197
  args = parser.parse_args()
192
198
  discussions = Discussion.objects
193
- if args['for']:
194
- discussions = discussions.generic_in(subject=args['for'])
195
- if args['user']:
196
- discussions = discussions(discussion__posted_by=ObjectId(args['user']))
197
- if args['closed'] is False:
199
+ if args["for"]:
200
+ discussions = discussions.generic_in(subject=args["for"])
201
+ if args["user"]:
202
+ discussions = discussions(discussion__posted_by=ObjectId(args["user"]))
203
+ if args["closed"] is False:
198
204
  discussions = discussions(closed=None)
199
- elif args['closed'] is True:
205
+ elif args["closed"] is True:
200
206
  discussions = discussions(closed__ne=None)
201
- discussions = discussions.order_by(args['sort'])
202
- return discussions.paginate(args['page'], args['page_size'])
207
+ discussions = discussions.order_by(args["sort"])
208
+ return discussions.paginate(args["page"], args["page_size"])
203
209
 
204
210
  @api.secure
205
- @api.doc('create_discussion')
211
+ @api.doc("create_discussion")
206
212
  @api.expect(start_discussion_fields)
207
213
  @api.marshal_with(discussion_fields)
208
214
  def post(self):
209
- '''Create a new Discussion'''
215
+ """Create a new Discussion"""
210
216
  form = api.validate(DiscussionCreateForm)
211
217
 
212
- message = Message(
213
- content=form.comment.data,
214
- posted_by=current_user.id)
218
+ message = Message(content=form.comment.data, posted_by=current_user.id)
215
219
  discussion = Discussion(user=current_user.id, discussion=[message])
216
220
  form.populate_obj(discussion)
217
221
  discussion.save()
@@ -9,11 +9,11 @@ class DiscussionFactory(ModelFactory):
9
9
  class Meta:
10
10
  model = Discussion
11
11
 
12
- title = factory.Faker('sentence')
12
+ title = factory.Faker("sentence")
13
13
 
14
14
 
15
15
  class MessageDiscussionFactory(ModelFactory):
16
16
  class Meta:
17
17
  model = Message
18
18
 
19
- content = factory.Faker('sentence')
19
+ content = factory.Faker("sentence")
@@ -1,23 +1,25 @@
1
- from udata.forms import ModelForm, Form, fields, validators
1
+ from udata.forms import Form, ModelForm, fields, validators
2
2
  from udata.i18n import lazy_gettext as _
3
3
 
4
- from .models import Discussion
5
4
  from .constants import COMMENT_SIZE_LIMIT
5
+ from .models import Discussion
6
6
 
7
- __all__ = ('DiscussionCreateForm', 'DiscussionCommentForm')
7
+ __all__ = ("DiscussionCreateForm", "DiscussionCommentForm")
8
8
 
9
9
 
10
10
  class DiscussionCreateForm(ModelForm):
11
11
  model_class = Discussion
12
12
 
13
- title = fields.StringField(_('Title'), [validators.DataRequired()])
13
+ title = fields.StringField(_("Title"), [validators.DataRequired()])
14
14
  comment = fields.StringField(
15
- _('Comment'), [validators.DataRequired(), validators.Length(max=COMMENT_SIZE_LIMIT)])
16
- subject = fields.ModelField(_('Subject'), [validators.DataRequired()])
15
+ _("Comment"), [validators.DataRequired(), validators.Length(max=COMMENT_SIZE_LIMIT)]
16
+ )
17
+ subject = fields.ModelField(_("Subject"), [validators.DataRequired()])
17
18
  extras = fields.ExtrasField()
18
19
 
19
20
 
20
21
  class DiscussionCommentForm(Form):
21
22
  comment = fields.StringField(
22
- _('Comment'), [validators.DataRequired(), validators.Length(max=COMMENT_SIZE_LIMIT)])
23
+ _("Comment"), [validators.DataRequired(), validators.Length(max=COMMENT_SIZE_LIMIT)]
24
+ )
23
25
  close = fields.BooleanField(default=False)
@@ -1,6 +1,4 @@
1
- from .signals import (
2
- on_new_discussion, on_discussion_closed, on_discussion_deleted
3
- )
1
+ from .signals import on_discussion_closed, on_discussion_deleted, on_new_discussion
4
2
 
5
3
 
6
4
  @on_new_discussion.connect
@@ -3,9 +3,10 @@ from datetime import datetime
3
3
 
4
4
  from flask_login import current_user
5
5
 
6
- from udata.mongo import db
7
6
  from udata.core.spam.models import SpamMixin, spam_protected
8
- from .signals import (on_new_discussion, on_discussion_closed, on_new_discussion_comment)
7
+ from udata.mongo import db
8
+
9
+ from .signals import on_discussion_closed, on_new_discussion, on_new_discussion_comment
9
10
 
10
11
  log = logging.getLogger(__name__)
11
12
 
@@ -13,23 +14,29 @@ log = logging.getLogger(__name__)
13
14
  class Message(SpamMixin, db.EmbeddedDocument):
14
15
  content = db.StringField(required=True)
15
16
  posted_on = db.DateTimeField(default=datetime.utcnow, required=True)
16
- posted_by = db.ReferenceField('User')
17
+ posted_by = db.ReferenceField("User")
17
18
 
18
19
  def texts_to_check_for_spam(self):
19
20
  return [self.content]
20
-
21
+
21
22
  def spam_report_message(self, breadcrumb):
22
- message = f"Spam potentiel dans le message"
23
+ message = f"Spam potentiel dans le message"
23
24
  if self.posted_by:
24
25
  message += f" de [{self.posted_by.fullname}]({self.posted_by.external_url})"
25
26
 
26
27
  if len(breadcrumb) != 2:
27
- log.warning(f"`spam_report_message` called on message with a breadcrumb of {len(breadcrumb)} elements.", extra={ 'breadcrumb': breadcrumb})
28
+ log.warning(
29
+ f"`spam_report_message` called on message with a breadcrumb of {len(breadcrumb)} elements.",
30
+ extra={"breadcrumb": breadcrumb},
31
+ )
28
32
  return message
29
-
33
+
30
34
  discussion = breadcrumb[0]
31
35
  if not isinstance(discussion, Discussion):
32
- log.warning(f"`spam_report_message` called on message with a breadcrumb not containing a Discussion at index 0.", extra={ 'breadcrumb': breadcrumb})
36
+ log.warning(
37
+ f"`spam_report_message` called on message with a breadcrumb not containing a Discussion at index 0.",
38
+ extra={"breadcrumb": breadcrumb},
39
+ )
33
40
  return message
34
41
 
35
42
  message += f" sur la discussion « [{discussion.title}]({discussion.external_url}) »"
@@ -37,22 +44,18 @@ class Message(SpamMixin, db.EmbeddedDocument):
37
44
 
38
45
 
39
46
  class Discussion(SpamMixin, db.Document):
40
- user = db.ReferenceField('User')
47
+ user = db.ReferenceField("User")
41
48
  subject = db.GenericReferenceField()
42
49
  title = db.StringField(required=True)
43
50
  discussion = db.ListField(db.EmbeddedDocumentField(Message))
44
51
  created = db.DateTimeField(default=datetime.utcnow, required=True)
45
52
  closed = db.DateTimeField()
46
- closed_by = db.ReferenceField('User')
53
+ closed_by = db.ReferenceField("User")
47
54
  extras = db.ExtrasField()
48
55
 
49
56
  meta = {
50
- 'indexes': [
51
- 'user',
52
- 'subject',
53
- '-created'
54
- ],
55
- 'ordering': ['-created'],
57
+ "indexes": ["user", "subject", "-created"],
58
+ "ordering": ["-created"],
56
59
  }
57
60
 
58
61
  def person_involved(self, person):
@@ -64,8 +67,8 @@ class Discussion(SpamMixin, db.Document):
64
67
 
65
68
  def texts_to_check_for_spam(self):
66
69
  # Discussion should always have a first message but it's not the case in some tests…
67
- return [self.title, self.discussion[0].content if len(self.discussion) else '']
68
-
70
+ return [self.title, self.discussion[0].content if len(self.discussion) else ""]
71
+
69
72
  def embeds_to_check_for_spam(self):
70
73
  return self.discussion[1:]
71
74
 
@@ -89,17 +92,15 @@ class Discussion(SpamMixin, db.Document):
89
92
 
90
93
  @property
91
94
  def external_url(self):
92
- return self.subject.url_for(
93
- _anchor='discussion-{id}'.format(id=self.id),
94
- _external=True)
95
-
95
+ return self.subject.url_for(_anchor="discussion-{id}".format(id=self.id), _external=True)
96
+
96
97
  def spam_report_message(self, breadcrumb):
97
- message = f"Spam potentiel sur la discussion « [{self.title}]({self.external_url}) »"
98
+ message = f"Spam potentiel sur la discussion « [{self.title}]({self.external_url}) »"
98
99
  if self.user:
99
100
  message += f" de [{self.user.fullname}]({self.user.external_url})"
100
101
 
101
102
  return message
102
-
103
+
103
104
  @spam_protected()
104
105
  def signal_new(self):
105
106
  on_new_discussion.send(self)
@@ -1,32 +1,36 @@
1
+ import logging
2
+
1
3
  from udata.features.notifications.actions import notifier
2
4
 
3
5
  from .actions import discussions_for
4
6
 
5
-
6
- import logging
7
-
8
7
  log = logging.getLogger(__name__)
9
8
 
10
9
 
11
- @notifier('discussion')
10
+ @notifier("discussion")
12
11
  def discussions_notifications(user):
13
- '''Notify user about open discussions'''
12
+ """Notify user about open discussions"""
14
13
  notifications = []
15
14
 
16
15
  # Only fetch required fields for notification serialization
17
16
  # Greatly improve performances and memory usage
18
- qs = discussions_for(user).only('id', 'created', 'title', 'subject')
17
+ qs = discussions_for(user).only("id", "created", "title", "subject")
19
18
 
20
19
  # Do not dereference subject (so it's a DBRef)
21
20
  # Also improve performances and memory usage
22
21
  for discussion in qs.no_dereference():
23
- notifications.append((discussion.created, {
24
- 'id': discussion.id,
25
- 'title': discussion.title,
26
- 'subject': {
27
- 'id': discussion.subject['_ref'].id,
28
- 'type': discussion.subject['_cls'].lower(),
29
- }
30
- }))
22
+ notifications.append(
23
+ (
24
+ discussion.created,
25
+ {
26
+ "id": discussion.id,
27
+ "title": discussion.title,
28
+ "subject": {
29
+ "id": discussion.subject["_ref"].id,
30
+ "type": discussion.subject["_cls"].lower(),
31
+ },
32
+ },
33
+ )
34
+ )
31
35
 
32
36
  return notifications
@@ -1,7 +1,7 @@
1
1
  from udata.auth import Permission, UserNeed
2
-
3
2
  from udata.core.organization.permissions import (
4
- OrganizationAdminNeed, OrganizationEditorNeed
3
+ OrganizationAdminNeed,
4
+ OrganizationEditorNeed,
5
5
  )
6
6
 
7
7
 
@@ -10,7 +10,7 @@ class CloseDiscussionPermission(Permission):
10
10
  needs = []
11
11
  subject = discussion.subject
12
12
 
13
- if getattr(subject, 'organization'):
13
+ if getattr(subject, "organization"):
14
14
  needs.append(OrganizationAdminNeed(subject.organization.id))
15
15
  needs.append(OrganizationEditorNeed(subject.organization.id))
16
16
  elif subject.owner:
@@ -3,14 +3,14 @@ from blinker import Namespace
3
3
  namespace = Namespace()
4
4
 
5
5
  #: Trigerred when an discussion is created
6
- on_new_discussion = namespace.signal('on-new-discussion')
6
+ on_new_discussion = namespace.signal("on-new-discussion")
7
7
 
8
8
  #: Trigerred when a new comment is posted on an discussion
9
9
  # (excluding creation and closing)
10
- on_new_discussion_comment = namespace.signal('on-new-discussion-comment')
10
+ on_new_discussion_comment = namespace.signal("on-new-discussion-comment")
11
11
 
12
12
  #: Trigerred when an discussion is closed
13
- on_discussion_closed = namespace.signal('on-discussion-closed')
13
+ on_discussion_closed = namespace.signal("on-discussion-closed")
14
14
 
15
15
  #: Trigerred when an discussion is deleted
16
- on_discussion_deleted = namespace.signal('on-discussion-deleted')
16
+ on_discussion_deleted = namespace.signal("on-discussion-deleted")