wbcore 2.2.1__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.
Files changed (1035) hide show
  1. wbcore/__init__.py +1 -0
  2. wbcore/admin.py +197 -0
  3. wbcore/apps.py +17 -0
  4. wbcore/cache/__init__.py +0 -0
  5. wbcore/cache/buttons.py +22 -0
  6. wbcore/cache/decorators.py +29 -0
  7. wbcore/cache/mixins.py +48 -0
  8. wbcore/cache/registry.py +90 -0
  9. wbcore/cache/views.py +18 -0
  10. wbcore/configs/__init__.py +11 -0
  11. wbcore/configs/configs.py +51 -0
  12. wbcore/configs/decorators.py +11 -0
  13. wbcore/configs/registry.py +35 -0
  14. wbcore/configs/views.py +11 -0
  15. wbcore/configurations/__init__.py +1 -0
  16. wbcore/configurations/base.py +45 -0
  17. wbcore/configurations/configurations/__init__.py +16 -0
  18. wbcore/configurations/configurations/apps.py +54 -0
  19. wbcore/configurations/configurations/authentication.py +43 -0
  20. wbcore/configurations/configurations/base.py +15 -0
  21. wbcore/configurations/configurations/cache.py +20 -0
  22. wbcore/configurations/configurations/celery.py +26 -0
  23. wbcore/configurations/configurations/i18nl10n.py +10 -0
  24. wbcore/configurations/configurations/mail.py +2 -0
  25. wbcore/configurations/configurations/maintenance.py +53 -0
  26. wbcore/configurations/configurations/media.py +22 -0
  27. wbcore/configurations/configurations/middleware.py +27 -0
  28. wbcore/configurations/configurations/network.py +19 -0
  29. wbcore/configurations/configurations/rest_framework.py +42 -0
  30. wbcore/configurations/configurations/static.py +28 -0
  31. wbcore/configurations/configurations/templates.py +17 -0
  32. wbcore/configurations/configurations/uvicorn.py +9 -0
  33. wbcore/configurations/configurations/wbcore.py +68 -0
  34. wbcore/content_type/__init__.py +0 -0
  35. wbcore/content_type/admin.py +8 -0
  36. wbcore/content_type/filters.py +19 -0
  37. wbcore/content_type/serializers.py +89 -0
  38. wbcore/content_type/utils.py +30 -0
  39. wbcore/content_type/viewsets.py +80 -0
  40. wbcore/contrib/__init__.py +0 -0
  41. wbcore/contrib/agenda/__init__.py +0 -0
  42. wbcore/contrib/agenda/admin/__init__.py +2 -0
  43. wbcore/contrib/agenda/admin/calendar_item.py +13 -0
  44. wbcore/contrib/agenda/admin/conference_room.py +18 -0
  45. wbcore/contrib/agenda/apps.py +6 -0
  46. wbcore/contrib/agenda/configurations.py +11 -0
  47. wbcore/contrib/agenda/factories/__init__.py +2 -0
  48. wbcore/contrib/agenda/factories/calendar_item.py +47 -0
  49. wbcore/contrib/agenda/factories/conference_room.py +19 -0
  50. wbcore/contrib/agenda/filters/__init__.py +2 -0
  51. wbcore/contrib/agenda/filters/calendar_item.py +64 -0
  52. wbcore/contrib/agenda/filters/conference_room.py +41 -0
  53. wbcore/contrib/agenda/migrations/0001_initial.py +84 -0
  54. wbcore/contrib/agenda/migrations/0002_initial.py +26 -0
  55. wbcore/contrib/agenda/migrations/0003_calendaritem_endpoint_basename.py +42 -0
  56. wbcore/contrib/agenda/migrations/0004_alter_calendaritem_item_type.py +17 -0
  57. wbcore/contrib/agenda/migrations/0005_building_and_more.py +94 -0
  58. wbcore/contrib/agenda/migrations/0006_calendaritem_is_deletable.py +17 -0
  59. wbcore/contrib/agenda/migrations/0007_alter_calendaritem_options.py +21 -0
  60. wbcore/contrib/agenda/migrations/0008_alter_calendaritem_item_type.py +17 -0
  61. wbcore/contrib/agenda/migrations/0009_alter_calendaritem_icon.py +18 -0
  62. wbcore/contrib/agenda/migrations/__init__.py +0 -0
  63. wbcore/contrib/agenda/models/__init__.py +2 -0
  64. wbcore/contrib/agenda/models/calendar_item.py +238 -0
  65. wbcore/contrib/agenda/models/conference_room.py +95 -0
  66. wbcore/contrib/agenda/release_notes/1_0_0.md +13 -0
  67. wbcore/contrib/agenda/release_notes/__init__.py +0 -0
  68. wbcore/contrib/agenda/serializers/__init__.py +10 -0
  69. wbcore/contrib/agenda/serializers/calendar_item.py +76 -0
  70. wbcore/contrib/agenda/serializers/conference_room.py +98 -0
  71. wbcore/contrib/agenda/signals.py +8 -0
  72. wbcore/contrib/agenda/tests/__init__.py +0 -0
  73. wbcore/contrib/agenda/tests/conftest.py +14 -0
  74. wbcore/contrib/agenda/tests/signals.py +16 -0
  75. wbcore/contrib/agenda/tests/test_models.py +34 -0
  76. wbcore/contrib/agenda/tests/test_viewsets.py +171 -0
  77. wbcore/contrib/agenda/tests/tests.py +25 -0
  78. wbcore/contrib/agenda/typings.py +19 -0
  79. wbcore/contrib/agenda/urls.py +25 -0
  80. wbcore/contrib/agenda/viewsets/__init__.py +13 -0
  81. wbcore/contrib/agenda/viewsets/buttons/__init__.py +1 -0
  82. wbcore/contrib/agenda/viewsets/buttons/conference_room.py +20 -0
  83. wbcore/contrib/agenda/viewsets/calendar_items.py +168 -0
  84. wbcore/contrib/agenda/viewsets/conference_room.py +48 -0
  85. wbcore/contrib/agenda/viewsets/display/__init__.py +2 -0
  86. wbcore/contrib/agenda/viewsets/display/calendar_items.py +17 -0
  87. wbcore/contrib/agenda/viewsets/display/conference_room.py +41 -0
  88. wbcore/contrib/agenda/viewsets/endpoints/__init__.py +1 -0
  89. wbcore/contrib/agenda/viewsets/endpoints/calendar_items.py +18 -0
  90. wbcore/contrib/agenda/viewsets/menu/__init__.py +2 -0
  91. wbcore/contrib/agenda/viewsets/menu/calendar_items.py +17 -0
  92. wbcore/contrib/agenda/viewsets/menu/conference_room.py +37 -0
  93. wbcore/contrib/agenda/viewsets/titles/__init__.py +2 -0
  94. wbcore/contrib/agenda/viewsets/titles/calendar_items.py +7 -0
  95. wbcore/contrib/agenda/viewsets/titles/conference_room.py +24 -0
  96. wbcore/contrib/ai/__init__.py +0 -0
  97. wbcore/contrib/ai/apps.py +5 -0
  98. wbcore/contrib/ai/exceptions.py +42 -0
  99. wbcore/contrib/ai/llm/__init__.py +0 -0
  100. wbcore/contrib/ai/llm/config.py +107 -0
  101. wbcore/contrib/ai/llm/decorators.py +10 -0
  102. wbcore/contrib/ai/llm/mixins.py +35 -0
  103. wbcore/contrib/ai/llm/utils.py +45 -0
  104. wbcore/contrib/authentication/__init__.py +9 -0
  105. wbcore/contrib/authentication/admin.py +247 -0
  106. wbcore/contrib/authentication/apps.py +14 -0
  107. wbcore/contrib/authentication/authentication.py +162 -0
  108. wbcore/contrib/authentication/configs.py +9 -0
  109. wbcore/contrib/authentication/configurations.py +53 -0
  110. wbcore/contrib/authentication/dynamic_preferences_registry.py +26 -0
  111. wbcore/contrib/authentication/factories/__init__.py +3 -0
  112. wbcore/contrib/authentication/factories/tokens.py +15 -0
  113. wbcore/contrib/authentication/factories/users.py +82 -0
  114. wbcore/contrib/authentication/factories/users_activities.py +19 -0
  115. wbcore/contrib/authentication/filters.py +18 -0
  116. wbcore/contrib/authentication/management/__init__.py +14 -0
  117. wbcore/contrib/authentication/migrations/0001_initial_squashed.py +174 -0
  118. wbcore/contrib/authentication/migrations/0002_profile.py +26 -0
  119. wbcore/contrib/authentication/migrations/0003_alter_user_profile.py +34 -0
  120. wbcore/contrib/authentication/migrations/0004_token.py +51 -0
  121. wbcore/contrib/authentication/migrations/0005_user_external_calendar_settings.py +17 -0
  122. wbcore/contrib/authentication/migrations/0006_auto_20231206_1422.py +13 -0
  123. wbcore/contrib/authentication/migrations/__init__.py +0 -0
  124. wbcore/contrib/authentication/models/__init__.py +3 -0
  125. wbcore/contrib/authentication/models/tokens.py +140 -0
  126. wbcore/contrib/authentication/models/users.py +225 -0
  127. wbcore/contrib/authentication/models/users_activities.py +112 -0
  128. wbcore/contrib/authentication/release_notes/1_0_0.md +13 -0
  129. wbcore/contrib/authentication/release_notes/__init__.py +0 -0
  130. wbcore/contrib/authentication/serializers/__init__.py +14 -0
  131. wbcore/contrib/authentication/serializers/users.py +350 -0
  132. wbcore/contrib/authentication/serializers/users_activites.py +37 -0
  133. wbcore/contrib/authentication/tasks.py +29 -0
  134. wbcore/contrib/authentication/tests/__init__.py +0 -0
  135. wbcore/contrib/authentication/tests/conftest.py +18 -0
  136. wbcore/contrib/authentication/tests/e2e/__init__.py +1 -0
  137. wbcore/contrib/authentication/tests/e2e/e2e_auth_utility.py +20 -0
  138. wbcore/contrib/authentication/tests/signals.py +17 -0
  139. wbcore/contrib/authentication/tests/test_configs.py +6 -0
  140. wbcore/contrib/authentication/tests/test_serializers.py +6 -0
  141. wbcore/contrib/authentication/tests/test_tasks.py +33 -0
  142. wbcore/contrib/authentication/tests/test_tokens.py +126 -0
  143. wbcore/contrib/authentication/tests/test_users.py +338 -0
  144. wbcore/contrib/authentication/tests/test_viewsets.py +6 -0
  145. wbcore/contrib/authentication/tests/tests.py +15 -0
  146. wbcore/contrib/authentication/urls.py +97 -0
  147. wbcore/contrib/authentication/utils.py +10 -0
  148. wbcore/contrib/authentication/viewsets/__init__.py +19 -0
  149. wbcore/contrib/authentication/viewsets/buttons/__init__.py +1 -0
  150. wbcore/contrib/authentication/viewsets/buttons/users.py +55 -0
  151. wbcore/contrib/authentication/viewsets/display/__init__.py +6 -0
  152. wbcore/contrib/authentication/viewsets/display/user_activities.py +85 -0
  153. wbcore/contrib/authentication/viewsets/display/users.py +72 -0
  154. wbcore/contrib/authentication/viewsets/endpoints/__init__.py +6 -0
  155. wbcore/contrib/authentication/viewsets/endpoints/user_activities.py +22 -0
  156. wbcore/contrib/authentication/viewsets/endpoints/users.py +20 -0
  157. wbcore/contrib/authentication/viewsets/menu/__init__.py +6 -0
  158. wbcore/contrib/authentication/viewsets/menu/user_activities.py +39 -0
  159. wbcore/contrib/authentication/viewsets/menu/users.py +16 -0
  160. wbcore/contrib/authentication/viewsets/titles/__init__.py +12 -0
  161. wbcore/contrib/authentication/viewsets/titles/user_activities.py +31 -0
  162. wbcore/contrib/authentication/viewsets/titles/users.py +26 -0
  163. wbcore/contrib/authentication/viewsets/user_activities.py +222 -0
  164. wbcore/contrib/authentication/viewsets/users.py +328 -0
  165. wbcore/contrib/color/migrations/0001_initial.py +33 -0
  166. wbcore/contrib/color/migrations/0002_alter_colorgradient_colors.py +25 -0
  167. wbcore/contrib/color/migrations/__init__.py +0 -0
  168. wbcore/contrib/currency/__init__.py +0 -0
  169. wbcore/contrib/currency/admin.py +32 -0
  170. wbcore/contrib/currency/apps.py +5 -0
  171. wbcore/contrib/currency/dynamic_preferences_registry.py +39 -0
  172. wbcore/contrib/currency/factories.py +39 -0
  173. wbcore/contrib/currency/import_export/__init__.py +0 -0
  174. wbcore/contrib/currency/import_export/backends/__init__.py +1 -0
  175. wbcore/contrib/currency/import_export/backends/fixerio/__init__.py +1 -0
  176. wbcore/contrib/currency/import_export/backends/fixerio/currency_fx_rates.py +66 -0
  177. wbcore/contrib/currency/import_export/backends/utils.py +5 -0
  178. wbcore/contrib/currency/import_export/handlers/__init__.py +2 -0
  179. wbcore/contrib/currency/import_export/handlers/currency.py +24 -0
  180. wbcore/contrib/currency/import_export/handlers/currency_fx_rates.py +28 -0
  181. wbcore/contrib/currency/import_export/parsers/__init__.py +0 -0
  182. wbcore/contrib/currency/import_export/parsers/fixerio/__init__.py +0 -0
  183. wbcore/contrib/currency/import_export/parsers/fixerio/currency_fx_rates.py +34 -0
  184. wbcore/contrib/currency/migrations/0001_initial.py +89 -0
  185. wbcore/contrib/currency/migrations/__init__.py +0 -0
  186. wbcore/contrib/currency/models.py +176 -0
  187. wbcore/contrib/currency/release_notes/1_0_0.md +13 -0
  188. wbcore/contrib/currency/release_notes/__init__.py +0 -0
  189. wbcore/contrib/currency/serializers.py +40 -0
  190. wbcore/contrib/currency/tests/__init__.py +0 -0
  191. wbcore/contrib/currency/tests/conftest.py +7 -0
  192. wbcore/contrib/currency/tests/test_models.py +104 -0
  193. wbcore/contrib/currency/tests/test_serializers.py +62 -0
  194. wbcore/contrib/currency/tests/test_viewsets.py +197 -0
  195. wbcore/contrib/currency/urls.py +19 -0
  196. wbcore/contrib/currency/viewsets/__init__.py +2 -0
  197. wbcore/contrib/currency/viewsets/buttons/__init__.py +0 -0
  198. wbcore/contrib/currency/viewsets/currency.py +56 -0
  199. wbcore/contrib/currency/viewsets/currency_fx_rates.py +32 -0
  200. wbcore/contrib/currency/viewsets/display/__init__.py +2 -0
  201. wbcore/contrib/currency/viewsets/display/currency.py +37 -0
  202. wbcore/contrib/currency/viewsets/display/currency_fx_rates.py +9 -0
  203. wbcore/contrib/currency/viewsets/endpoints/__init__.py +1 -0
  204. wbcore/contrib/currency/viewsets/endpoints/currency_fx_rates.py +14 -0
  205. wbcore/contrib/currency/viewsets/menu/__init__.py +1 -0
  206. wbcore/contrib/currency/viewsets/menu/currency.py +8 -0
  207. wbcore/contrib/currency/viewsets/preview/__init__.py +1 -0
  208. wbcore/contrib/currency/viewsets/preview/currency.py +10 -0
  209. wbcore/contrib/currency/viewsets/titles/__init__.py +2 -0
  210. wbcore/contrib/currency/viewsets/titles/currency.py +6 -0
  211. wbcore/contrib/currency/viewsets/titles/currency_fx_rates.py +8 -0
  212. wbcore/contrib/dataloader/__init__.py +1 -0
  213. wbcore/contrib/dataloader/apps.py +5 -0
  214. wbcore/contrib/dataloader/dataloaders/__init__.py +2 -0
  215. wbcore/contrib/dataloader/dataloaders/dataloaders.py +24 -0
  216. wbcore/contrib/dataloader/dataloaders/proxies.py +81 -0
  217. wbcore/contrib/dataloader/models/__init__.py +1 -0
  218. wbcore/contrib/dataloader/models/entities.py +29 -0
  219. wbcore/contrib/dataloader/models/querysets/__init__.py +1 -0
  220. wbcore/contrib/dataloader/models/querysets/entities.py +27 -0
  221. wbcore/contrib/dataloader/tests/__init__.py +0 -0
  222. wbcore/contrib/dataloader/tests/conftest.py +26 -0
  223. wbcore/contrib/dataloader/tests/test/__init__.py +0 -0
  224. wbcore/contrib/dataloader/tests/test/dataloaders/__init__.py +0 -0
  225. wbcore/contrib/dataloader/tests/test/dataloaders/dataloaders.py +18 -0
  226. wbcore/contrib/dataloader/tests/test/dataloaders/protocols.py +6 -0
  227. wbcore/contrib/dataloader/tests/test/dataloaders/proxies.py +11 -0
  228. wbcore/contrib/dataloader/tests/test/factories.py +13 -0
  229. wbcore/contrib/dataloader/tests/test/models.py +10 -0
  230. wbcore/contrib/dataloader/tests/test_dataloaders.py +37 -0
  231. wbcore/contrib/dataloader/tests/test_entities.py +12 -0
  232. wbcore/contrib/dataloader/utils.py +17 -0
  233. wbcore/contrib/directory/__init__.py +0 -0
  234. wbcore/contrib/directory/admin/__init__.py +14 -0
  235. wbcore/contrib/directory/admin/contacts.py +54 -0
  236. wbcore/contrib/directory/admin/entries.py +230 -0
  237. wbcore/contrib/directory/admin/relationships.py +73 -0
  238. wbcore/contrib/directory/apps.py +6 -0
  239. wbcore/contrib/directory/configs.py +8 -0
  240. wbcore/contrib/directory/configurations.py +69 -0
  241. wbcore/contrib/directory/dynamic_preferences_registry.py +61 -0
  242. wbcore/contrib/directory/factories/__init__.py +34 -0
  243. wbcore/contrib/directory/factories/contacts.py +71 -0
  244. wbcore/contrib/directory/factories/entries.py +169 -0
  245. wbcore/contrib/directory/factories/relationships.py +77 -0
  246. wbcore/contrib/directory/filters/__init__.py +22 -0
  247. wbcore/contrib/directory/filters/contacts.py +233 -0
  248. wbcore/contrib/directory/filters/entries.py +247 -0
  249. wbcore/contrib/directory/filters/relationships.py +89 -0
  250. wbcore/contrib/directory/migrations/0001_initial.py +871 -0
  251. wbcore/contrib/directory/migrations/0002_auto_20230414_1553.py +42 -0
  252. wbcore/contrib/directory/migrations/0003_remove_entry_last_event.py +16 -0
  253. wbcore/contrib/directory/migrations/0004_entry_is_draft_entry.py +21 -0
  254. wbcore/contrib/directory/migrations/0005_entry_salutation.py +17 -0
  255. wbcore/contrib/directory/migrations/0006_employeremployeerelationship_position_name.py +23 -0
  256. wbcore/contrib/directory/migrations/0007_alter_bankingcontact_options.py +20 -0
  257. wbcore/contrib/directory/migrations/0008_bankingcontact_access.py +17 -0
  258. wbcore/contrib/directory/migrations/0009_remove_entry_external_identfier_and_more.py +22 -0
  259. wbcore/contrib/directory/migrations/__init__.py +0 -0
  260. wbcore/contrib/directory/models/__init__.py +27 -0
  261. wbcore/contrib/directory/models/contacts.py +554 -0
  262. wbcore/contrib/directory/models/entries.py +889 -0
  263. wbcore/contrib/directory/models/relationships.py +634 -0
  264. wbcore/contrib/directory/preferences.py +10 -0
  265. wbcore/contrib/directory/release_notes/1_0_0.md +13 -0
  266. wbcore/contrib/directory/release_notes/1_0_1.md +13 -0
  267. wbcore/contrib/directory/release_notes/__init__.py +0 -0
  268. wbcore/contrib/directory/serializers/__init__.py +63 -0
  269. wbcore/contrib/directory/serializers/companies.py +158 -0
  270. wbcore/contrib/directory/serializers/contacts.py +404 -0
  271. wbcore/contrib/directory/serializers/entries.py +344 -0
  272. wbcore/contrib/directory/serializers/entry_representations.py +35 -0
  273. wbcore/contrib/directory/serializers/persons.py +209 -0
  274. wbcore/contrib/directory/serializers/relationships.py +332 -0
  275. wbcore/contrib/directory/signals.py +3 -0
  276. wbcore/contrib/directory/tests/__init__.py +0 -0
  277. wbcore/contrib/directory/tests/conftest.py +60 -0
  278. wbcore/contrib/directory/tests/disable_signals.py +52 -0
  279. wbcore/contrib/directory/tests/e2e/__init__.py +7 -0
  280. wbcore/contrib/directory/tests/e2e/e2e_directory_utility.py +162 -0
  281. wbcore/contrib/directory/tests/signals.py +90 -0
  282. wbcore/contrib/directory/tests/test_configs.py +6 -0
  283. wbcore/contrib/directory/tests/test_filters.py +59 -0
  284. wbcore/contrib/directory/tests/test_models.py +285 -0
  285. wbcore/contrib/directory/tests/test_permissions.py +122 -0
  286. wbcore/contrib/directory/tests/test_serializers.py +201 -0
  287. wbcore/contrib/directory/tests/test_viewsets.py +677 -0
  288. wbcore/contrib/directory/tests/tests.py +130 -0
  289. wbcore/contrib/directory/typings.py +17 -0
  290. wbcore/contrib/directory/urls.py +135 -0
  291. wbcore/contrib/directory/viewsets/__init__.py +58 -0
  292. wbcore/contrib/directory/viewsets/buttons/__init__.py +7 -0
  293. wbcore/contrib/directory/viewsets/buttons/contacts.py +16 -0
  294. wbcore/contrib/directory/viewsets/buttons/entries.py +133 -0
  295. wbcore/contrib/directory/viewsets/buttons/relationships.py +31 -0
  296. wbcore/contrib/directory/viewsets/contacts.py +393 -0
  297. wbcore/contrib/directory/viewsets/display/__init__.py +36 -0
  298. wbcore/contrib/directory/viewsets/display/contacts.py +380 -0
  299. wbcore/contrib/directory/viewsets/display/entries.py +501 -0
  300. wbcore/contrib/directory/viewsets/display/relationships.py +315 -0
  301. wbcore/contrib/directory/viewsets/display/utils.py +33 -0
  302. wbcore/contrib/directory/viewsets/endpoints/__init__.py +22 -0
  303. wbcore/contrib/directory/viewsets/endpoints/contacts.py +54 -0
  304. wbcore/contrib/directory/viewsets/endpoints/entries.py +30 -0
  305. wbcore/contrib/directory/viewsets/endpoints/relationships.py +105 -0
  306. wbcore/contrib/directory/viewsets/entries.py +212 -0
  307. wbcore/contrib/directory/viewsets/menu/__init__.py +16 -0
  308. wbcore/contrib/directory/viewsets/menu/contacts.py +25 -0
  309. wbcore/contrib/directory/viewsets/menu/entries.py +61 -0
  310. wbcore/contrib/directory/viewsets/menu/relationships.py +31 -0
  311. wbcore/contrib/directory/viewsets/menu/utils.py +72 -0
  312. wbcore/contrib/directory/viewsets/mixins.py +11 -0
  313. wbcore/contrib/directory/viewsets/previews/__init__.py +2 -0
  314. wbcore/contrib/directory/viewsets/previews/contacts.py +18 -0
  315. wbcore/contrib/directory/viewsets/previews/entries.py +31 -0
  316. wbcore/contrib/directory/viewsets/relationships.py +241 -0
  317. wbcore/contrib/directory/viewsets/titles/__init__.py +30 -0
  318. wbcore/contrib/directory/viewsets/titles/contacts.py +122 -0
  319. wbcore/contrib/directory/viewsets/titles/entries.py +29 -0
  320. wbcore/contrib/directory/viewsets/titles/relationships.py +86 -0
  321. wbcore/contrib/directory/viewsets/titles/utils.py +46 -0
  322. wbcore/contrib/directory/viewsets/utils.py +101 -0
  323. wbcore/contrib/documents/__init__.py +0 -0
  324. wbcore/contrib/documents/admin.py +79 -0
  325. wbcore/contrib/documents/apps.py +6 -0
  326. wbcore/contrib/documents/factories.py +43 -0
  327. wbcore/contrib/documents/filters.py +82 -0
  328. wbcore/contrib/documents/migrations/0001_initial.py +189 -0
  329. wbcore/contrib/documents/migrations/0002_documentmodelrelationship_primary_and_more.py +22 -0
  330. wbcore/contrib/documents/migrations/0003_alter_documentmodelrelationship_unique_together_and_more.py +30 -0
  331. wbcore/contrib/documents/migrations/0004_auto_20240103_0958.py +44 -0
  332. wbcore/contrib/documents/migrations/0005_document_valid_from_document_valid_until_and_more.py +32 -0
  333. wbcore/contrib/documents/migrations/__init__.py +0 -0
  334. wbcore/contrib/documents/models/__init__.py +4 -0
  335. wbcore/contrib/documents/models/document_model_relationships.py +57 -0
  336. wbcore/contrib/documents/models/document_types.py +35 -0
  337. wbcore/contrib/documents/models/documents.py +309 -0
  338. wbcore/contrib/documents/models/mixins.py +10 -0
  339. wbcore/contrib/documents/models/shareable_links.py +90 -0
  340. wbcore/contrib/documents/release_notes/1_0_0.md +13 -0
  341. wbcore/contrib/documents/release_notes/__init__.py +0 -0
  342. wbcore/contrib/documents/serializers/__init__.py +12 -0
  343. wbcore/contrib/documents/serializers/document_model_relationships.py +31 -0
  344. wbcore/contrib/documents/serializers/document_types.py +40 -0
  345. wbcore/contrib/documents/serializers/documents.py +64 -0
  346. wbcore/contrib/documents/serializers/shareable_links.py +90 -0
  347. wbcore/contrib/documents/urls.py +42 -0
  348. wbcore/contrib/documents/viewsets/__init__.py +10 -0
  349. wbcore/contrib/documents/viewsets/buttons/__init__.py +3 -0
  350. wbcore/contrib/documents/viewsets/buttons/documents.py +53 -0
  351. wbcore/contrib/documents/viewsets/buttons/shareable_links.py +20 -0
  352. wbcore/contrib/documents/viewsets/buttons/signals.py +13 -0
  353. wbcore/contrib/documents/viewsets/display/__init__.py +4 -0
  354. wbcore/contrib/documents/viewsets/display/document_model_relationships.py +18 -0
  355. wbcore/contrib/documents/viewsets/display/document_types.py +23 -0
  356. wbcore/contrib/documents/viewsets/display/documents.py +101 -0
  357. wbcore/contrib/documents/viewsets/display/shareable_links.py +65 -0
  358. wbcore/contrib/documents/viewsets/document_model_relationships.py +25 -0
  359. wbcore/contrib/documents/viewsets/document_types.py +37 -0
  360. wbcore/contrib/documents/viewsets/documents.py +107 -0
  361. wbcore/contrib/documents/viewsets/endpoints/__init__.py +5 -0
  362. wbcore/contrib/documents/viewsets/endpoints/documents.py +19 -0
  363. wbcore/contrib/documents/viewsets/endpoints/documents_model_relationships.py +13 -0
  364. wbcore/contrib/documents/viewsets/endpoints/shareable_links.py +45 -0
  365. wbcore/contrib/documents/viewsets/menu/__init__.py +1 -0
  366. wbcore/contrib/documents/viewsets/menu/documents.py +24 -0
  367. wbcore/contrib/documents/viewsets/previews/__init__.py +1 -0
  368. wbcore/contrib/documents/viewsets/previews/documents.py +10 -0
  369. wbcore/contrib/documents/viewsets/shareable_links.py +109 -0
  370. wbcore/contrib/documents/viewsets/titles/__init__.py +3 -0
  371. wbcore/contrib/documents/viewsets/titles/document_types.py +13 -0
  372. wbcore/contrib/documents/viewsets/titles/documents.py +26 -0
  373. wbcore/contrib/documents/viewsets/titles/shareable_links.py +13 -0
  374. wbcore/contrib/dynamic_preferences/__init__.py +0 -0
  375. wbcore/contrib/dynamic_preferences/types.py +19 -0
  376. wbcore/contrib/example_app/__init__.py +0 -0
  377. wbcore/contrib/example_app/admin.py +117 -0
  378. wbcore/contrib/example_app/apps.py +6 -0
  379. wbcore/contrib/example_app/factories/__init__.py +8 -0
  380. wbcore/contrib/example_app/factories/event.py +26 -0
  381. wbcore/contrib/example_app/factories/league.py +15 -0
  382. wbcore/contrib/example_app/factories/match.py +25 -0
  383. wbcore/contrib/example_app/factories/person.py +32 -0
  384. wbcore/contrib/example_app/factories/role.py +9 -0
  385. wbcore/contrib/example_app/factories/sport.py +12 -0
  386. wbcore/contrib/example_app/factories/stadium.py +12 -0
  387. wbcore/contrib/example_app/factories/team.py +35 -0
  388. wbcore/contrib/example_app/filters/__init__.py +9 -0
  389. wbcore/contrib/example_app/filters/event.py +80 -0
  390. wbcore/contrib/example_app/filters/league.py +74 -0
  391. wbcore/contrib/example_app/filters/match.py +92 -0
  392. wbcore/contrib/example_app/filters/person.py +77 -0
  393. wbcore/contrib/example_app/filters/role.py +17 -0
  394. wbcore/contrib/example_app/filters/sport.py +26 -0
  395. wbcore/contrib/example_app/filters/stadium.py +30 -0
  396. wbcore/contrib/example_app/filters/team.py +77 -0
  397. wbcore/contrib/example_app/filters/teamresult.py +47 -0
  398. wbcore/contrib/example_app/migrations/0001_initial.py +498 -0
  399. wbcore/contrib/example_app/migrations/0002_sportperson_profile.py +49 -0
  400. wbcore/contrib/example_app/migrations/0003_change_stadium_capacity.py +31 -0
  401. wbcore/contrib/example_app/migrations/0004_alter_player_transfer_value.py +21 -0
  402. wbcore/contrib/example_app/migrations/0005_sportperson_profile_image.py +23 -0
  403. wbcore/contrib/example_app/migrations/0006_league_season_period_player_is_active_and_more.py +116 -0
  404. wbcore/contrib/example_app/migrations/0007_alter_player_options_alter_team_options_and_more.py +40 -0
  405. wbcore/contrib/example_app/migrations/__init__.py +0 -0
  406. wbcore/contrib/example_app/models.py +906 -0
  407. wbcore/contrib/example_app/serializers/__init__.py +31 -0
  408. wbcore/contrib/example_app/serializers/league.py +111 -0
  409. wbcore/contrib/example_app/serializers/match_event.py +304 -0
  410. wbcore/contrib/example_app/serializers/person_team.py +280 -0
  411. wbcore/contrib/example_app/serializers/role.py +16 -0
  412. wbcore/contrib/example_app/serializers/season.py +53 -0
  413. wbcore/contrib/example_app/serializers/sport.py +37 -0
  414. wbcore/contrib/example_app/serializers/stadium.py +58 -0
  415. wbcore/contrib/example_app/serializers/teamresult.py +43 -0
  416. wbcore/contrib/example_app/tests/__init__.py +0 -0
  417. wbcore/contrib/example_app/tests/conftest.py +13 -0
  418. wbcore/contrib/example_app/tests/e2e/__init__.py +1 -0
  419. wbcore/contrib/example_app/tests/e2e/e2e_example_app_utility.py +51 -0
  420. wbcore/contrib/example_app/tests/e2e/test_league.py +69 -0
  421. wbcore/contrib/example_app/tests/e2e/test_person.py +67 -0
  422. wbcore/contrib/example_app/tests/e2e/test_teams.py +56 -0
  423. wbcore/contrib/example_app/tests/signals.py +7 -0
  424. wbcore/contrib/example_app/tests/test_displays.py +40 -0
  425. wbcore/contrib/example_app/tests/test_filters.py +70 -0
  426. wbcore/contrib/example_app/tests/test_utils.py +25 -0
  427. wbcore/contrib/example_app/urls.py +79 -0
  428. wbcore/contrib/example_app/utils.py +20 -0
  429. wbcore/contrib/example_app/viewsets/__init__.py +29 -0
  430. wbcore/contrib/example_app/viewsets/buttons/__init__.py +2 -0
  431. wbcore/contrib/example_app/viewsets/buttons/person.py +18 -0
  432. wbcore/contrib/example_app/viewsets/buttons/team.py +23 -0
  433. wbcore/contrib/example_app/viewsets/displays/__init__.py +23 -0
  434. wbcore/contrib/example_app/viewsets/displays/event.py +186 -0
  435. wbcore/contrib/example_app/viewsets/displays/league.py +390 -0
  436. wbcore/contrib/example_app/viewsets/displays/match.py +264 -0
  437. wbcore/contrib/example_app/viewsets/displays/person.py +251 -0
  438. wbcore/contrib/example_app/viewsets/displays/role.py +16 -0
  439. wbcore/contrib/example_app/viewsets/displays/season.py +65 -0
  440. wbcore/contrib/example_app/viewsets/displays/sport.py +115 -0
  441. wbcore/contrib/example_app/viewsets/displays/stadium.py +149 -0
  442. wbcore/contrib/example_app/viewsets/displays/team.py +230 -0
  443. wbcore/contrib/example_app/viewsets/displays/teamresult.py +30 -0
  444. wbcore/contrib/example_app/viewsets/endpoints/__init__.py +12 -0
  445. wbcore/contrib/example_app/viewsets/endpoints/endpoints.py +79 -0
  446. wbcore/contrib/example_app/viewsets/event.py +287 -0
  447. wbcore/contrib/example_app/viewsets/league.py +72 -0
  448. wbcore/contrib/example_app/viewsets/match.py +119 -0
  449. wbcore/contrib/example_app/viewsets/menu/__init__.py +1 -0
  450. wbcore/contrib/example_app/viewsets/menu/menus.py +102 -0
  451. wbcore/contrib/example_app/viewsets/menus.py +63 -0
  452. wbcore/contrib/example_app/viewsets/person.py +133 -0
  453. wbcore/contrib/example_app/viewsets/role.py +25 -0
  454. wbcore/contrib/example_app/viewsets/season.py +23 -0
  455. wbcore/contrib/example_app/viewsets/sport.py +26 -0
  456. wbcore/contrib/example_app/viewsets/stadium.py +30 -0
  457. wbcore/contrib/example_app/viewsets/team.py +68 -0
  458. wbcore/contrib/example_app/viewsets/teamresult.py +106 -0
  459. wbcore/contrib/example_app/viewsets/titles/__init__.py +9 -0
  460. wbcore/contrib/example_app/viewsets/titles/event.py +41 -0
  461. wbcore/contrib/example_app/viewsets/titles/league.py +24 -0
  462. wbcore/contrib/example_app/viewsets/titles/match.py +34 -0
  463. wbcore/contrib/example_app/viewsets/titles/person.py +35 -0
  464. wbcore/contrib/example_app/viewsets/titles/role.py +13 -0
  465. wbcore/contrib/example_app/viewsets/titles/sport.py +13 -0
  466. wbcore/contrib/example_app/viewsets/titles/stadium.py +13 -0
  467. wbcore/contrib/example_app/viewsets/titles/team.py +24 -0
  468. wbcore/contrib/example_app/viewsets/titles/teamresult.py +12 -0
  469. wbcore/contrib/geography/__init__.py +0 -0
  470. wbcore/contrib/geography/admin.py +30 -0
  471. wbcore/contrib/geography/apps.py +5 -0
  472. wbcore/contrib/geography/factories.py +46 -0
  473. wbcore/contrib/geography/import_export/__init__.py +0 -0
  474. wbcore/contrib/geography/import_export/resources/__init__.py +0 -0
  475. wbcore/contrib/geography/import_export/resources/geography.py +10 -0
  476. wbcore/contrib/geography/migrations/0001_initial.py +110 -0
  477. wbcore/contrib/geography/migrations/__init__.py +0 -0
  478. wbcore/contrib/geography/models.py +147 -0
  479. wbcore/contrib/geography/release_notes/1_0_0.md +13 -0
  480. wbcore/contrib/geography/release_notes/__init__.py +0 -0
  481. wbcore/contrib/geography/serializers.py +32 -0
  482. wbcore/contrib/geography/tests/__init__.py +0 -0
  483. wbcore/contrib/geography/tests/conftest.py +12 -0
  484. wbcore/contrib/geography/tests/signals.py +7 -0
  485. wbcore/contrib/geography/tests/test_models.py +12 -0
  486. wbcore/contrib/geography/tests/test_serializers.py +7 -0
  487. wbcore/contrib/geography/tests/test_viewsets.py +6 -0
  488. wbcore/contrib/geography/tests/tests.py +12 -0
  489. wbcore/contrib/geography/urls.py +12 -0
  490. wbcore/contrib/geography/viewsets/__init__.py +1 -0
  491. wbcore/contrib/geography/viewsets/buttons/__init__.py +0 -0
  492. wbcore/contrib/geography/viewsets/display/__init__.py +1 -0
  493. wbcore/contrib/geography/viewsets/display/geography.py +32 -0
  494. wbcore/contrib/geography/viewsets/endpoints/__init__.py +0 -0
  495. wbcore/contrib/geography/viewsets/geography.py +57 -0
  496. wbcore/contrib/geography/viewsets/menu/__init__.py +1 -0
  497. wbcore/contrib/geography/viewsets/menu/geography.py +8 -0
  498. wbcore/contrib/geography/viewsets/preview/__init__.py +1 -0
  499. wbcore/contrib/geography/viewsets/preview/geography.py +19 -0
  500. wbcore/contrib/geography/viewsets/titles/__init__.py +0 -0
  501. wbcore/contrib/geography/viewsets/titles/geography.py +14 -0
  502. wbcore/contrib/gleap/__init__.py +0 -0
  503. wbcore/contrib/gleap/apps.py +5 -0
  504. wbcore/contrib/gleap/configs.py +11 -0
  505. wbcore/contrib/gleap/configurations.py +6 -0
  506. wbcore/contrib/gleap/hashes.py +13 -0
  507. wbcore/contrib/gleap/tests/__init__.py +0 -0
  508. wbcore/contrib/gleap/tests/conftest.py +1 -0
  509. wbcore/contrib/gleap/tests/tests.py +29 -0
  510. wbcore/contrib/gleap/urls.py +7 -0
  511. wbcore/contrib/gleap/views.py +31 -0
  512. wbcore/contrib/guardian/migrations/0001_initial.py +103 -0
  513. wbcore/contrib/guardian/migrations/__init__.py +0 -0
  514. wbcore/contrib/guardian/models/__init__.py +1 -0
  515. wbcore/contrib/guardian/models/mixins.py +138 -0
  516. wbcore/contrib/guardian/models/models.py +29 -0
  517. wbcore/contrib/guardian/tests/__init__.py +0 -0
  518. wbcore/contrib/guardian/tests/conftest.py +1 -0
  519. wbcore/contrib/guardian/tests/test_model_mixins.py +93 -0
  520. wbcore/contrib/guardian/tests/test_tasks.py +77 -0
  521. wbcore/contrib/guardian/tests/test_utils.py +196 -0
  522. wbcore/contrib/guardian/tests/test_viewsets.py +48 -0
  523. wbcore/contrib/guardian/viewsets/__init__.py +1 -0
  524. wbcore/contrib/guardian/viewsets/configs/__init__.py +4 -0
  525. wbcore/contrib/guardian/viewsets/configs/buttons.py +27 -0
  526. wbcore/contrib/guardian/viewsets/configs/displays.py +43 -0
  527. wbcore/contrib/guardian/viewsets/configs/endpoints.py +18 -0
  528. wbcore/contrib/guardian/viewsets/configs/titles.py +16 -0
  529. wbcore/contrib/guardian/viewsets/mixins.py +6 -0
  530. wbcore/contrib/guardian/viewsets/viewsets.py +139 -0
  531. wbcore/contrib/icons/__init__.py +1 -0
  532. wbcore/contrib/icons/apps.py +5 -0
  533. wbcore/contrib/icons/backends/__init__.py +5 -0
  534. wbcore/contrib/icons/backends/default.py +374 -0
  535. wbcore/contrib/icons/backends/material.py +135 -0
  536. wbcore/contrib/icons/icons.py +169 -0
  537. wbcore/contrib/icons/models.py +11 -0
  538. wbcore/contrib/icons/serializers.py +18 -0
  539. wbcore/contrib/io/__init__.py +0 -0
  540. wbcore/contrib/io/admin.py +150 -0
  541. wbcore/contrib/io/apps.py +27 -0
  542. wbcore/contrib/io/backends/__init__.py +5 -0
  543. wbcore/contrib/io/backends/abstract.py +60 -0
  544. wbcore/contrib/io/backends/mail.py +85 -0
  545. wbcore/contrib/io/backends/sftp.py +63 -0
  546. wbcore/contrib/io/backends/stream.py +92 -0
  547. wbcore/contrib/io/backends/utils.py +42 -0
  548. wbcore/contrib/io/configs/__init__.py +0 -0
  549. wbcore/contrib/io/configs/endpoints.py +12 -0
  550. wbcore/contrib/io/configurations/__init__.py +1 -0
  551. wbcore/contrib/io/configurations/base.py +7 -0
  552. wbcore/contrib/io/dynamic_preferences_registry.py +15 -0
  553. wbcore/contrib/io/enums.py +15 -0
  554. wbcore/contrib/io/exceptions.py +16 -0
  555. wbcore/contrib/io/factories.py +202 -0
  556. wbcore/contrib/io/imports.py +305 -0
  557. wbcore/contrib/io/management/__init__.py +13 -0
  558. wbcore/contrib/io/migrations/0001_initial_squashed.py +319 -0
  559. wbcore/contrib/io/migrations/0002_importsource_creator.py +26 -0
  560. wbcore/contrib/io/migrations/0003_auto_20240103_1000.py +46 -0
  561. wbcore/contrib/io/migrations/0004_alter_importsource_status_exportsource.py +134 -0
  562. wbcore/contrib/io/migrations/0005_exportsource_data_alter_exportsource_query_str_and_more.py +67 -0
  563. wbcore/contrib/io/migrations/0006_alter_exportsource_query_params.py +20 -0
  564. wbcore/contrib/io/migrations/0007_alter_exportsource_query_params.py +23 -0
  565. wbcore/contrib/io/migrations/__init__.py +0 -0
  566. wbcore/contrib/io/mixins.py +37 -0
  567. wbcore/contrib/io/models.py +1005 -0
  568. wbcore/contrib/io/release_notes/1_0_0.md +13 -0
  569. wbcore/contrib/io/release_notes/__init__.py +0 -0
  570. wbcore/contrib/io/resources.py +177 -0
  571. wbcore/contrib/io/serializers.py +136 -0
  572. wbcore/contrib/io/tests/__init__.py +0 -0
  573. wbcore/contrib/io/tests/conftest.py +42 -0
  574. wbcore/contrib/io/tests/test_backends.py +128 -0
  575. wbcore/contrib/io/tests/test_exports.py +130 -0
  576. wbcore/contrib/io/tests/test_imports.py +167 -0
  577. wbcore/contrib/io/tests/test_models.py +363 -0
  578. wbcore/contrib/io/tests/tests.py +18 -0
  579. wbcore/contrib/io/urls.py +28 -0
  580. wbcore/contrib/io/utils.py +44 -0
  581. wbcore/contrib/io/viewset_mixins.py +301 -0
  582. wbcore/contrib/io/viewsets.py +138 -0
  583. wbcore/contrib/notifications/__init__.py +0 -0
  584. wbcore/contrib/notifications/admin.py +61 -0
  585. wbcore/contrib/notifications/apps.py +38 -0
  586. wbcore/contrib/notifications/backends/__init__.py +0 -0
  587. wbcore/contrib/notifications/backends/abstract_backend.py +13 -0
  588. wbcore/contrib/notifications/backends/console/__init__.py +1 -0
  589. wbcore/contrib/notifications/backends/console/backends.py +25 -0
  590. wbcore/contrib/notifications/backends/firebase/__init__.py +1 -0
  591. wbcore/contrib/notifications/backends/firebase/backends.py +102 -0
  592. wbcore/contrib/notifications/configs.py +12 -0
  593. wbcore/contrib/notifications/configurations.py +9 -0
  594. wbcore/contrib/notifications/dispatch.py +71 -0
  595. wbcore/contrib/notifications/factories/__init__.py +0 -0
  596. wbcore/contrib/notifications/factories/notification_types.py +22 -0
  597. wbcore/contrib/notifications/factories/notifications.py +16 -0
  598. wbcore/contrib/notifications/factories/tokens.py +11 -0
  599. wbcore/contrib/notifications/migrations/0001_initial.py +116 -0
  600. wbcore/contrib/notifications/migrations/0002_notificationusertoken_unique_user_token_device.py +18 -0
  601. wbcore/contrib/notifications/migrations/0003_notificationusertoken_updated.py +17 -0
  602. wbcore/contrib/notifications/migrations/0004_alter_notification_body.py +17 -0
  603. wbcore/contrib/notifications/migrations/0005_alter_notification_endpoint.py +17 -0
  604. wbcore/contrib/notifications/migrations/0006_notification_created.py +25 -0
  605. wbcore/contrib/notifications/migrations/__init__.py +0 -0
  606. wbcore/contrib/notifications/models/__init__.py +3 -0
  607. wbcore/contrib/notifications/models/notification_types.py +112 -0
  608. wbcore/contrib/notifications/models/notifications.py +81 -0
  609. wbcore/contrib/notifications/models/tokens.py +45 -0
  610. wbcore/contrib/notifications/release_notes/1_0_0.md +13 -0
  611. wbcore/contrib/notifications/release_notes/__init__.py +0 -0
  612. wbcore/contrib/notifications/serializers/__init__.py +5 -0
  613. wbcore/contrib/notifications/serializers/notification_types.py +36 -0
  614. wbcore/contrib/notifications/serializers/notifications.py +33 -0
  615. wbcore/contrib/notifications/tasks.py +55 -0
  616. wbcore/contrib/notifications/tests/__init__.py +0 -0
  617. wbcore/contrib/notifications/tests/conftest.py +47 -0
  618. wbcore/contrib/notifications/tests/test_backends/__init__.py +0 -0
  619. wbcore/contrib/notifications/tests/test_backends/test_firebase.py +78 -0
  620. wbcore/contrib/notifications/tests/test_configs.py +7 -0
  621. wbcore/contrib/notifications/tests/test_models/__init__.py +0 -0
  622. wbcore/contrib/notifications/tests/test_models/test_notification_types.py +84 -0
  623. wbcore/contrib/notifications/tests/test_models/test_notifications.py +45 -0
  624. wbcore/contrib/notifications/tests/test_models/test_tokens.py +21 -0
  625. wbcore/contrib/notifications/tests/test_serializers/__init__.py +0 -0
  626. wbcore/contrib/notifications/tests/test_serializers/test_notification_types.py +53 -0
  627. wbcore/contrib/notifications/tests/test_serializers/test_notifications.py +23 -0
  628. wbcore/contrib/notifications/tests/test_tasks.py +71 -0
  629. wbcore/contrib/notifications/tests/test_utils.py +0 -0
  630. wbcore/contrib/notifications/tests/test_viewsets/__init__.py +0 -0
  631. wbcore/contrib/notifications/tests/test_viewsets/test_notification_types.py +121 -0
  632. wbcore/contrib/notifications/tests/test_viewsets/test_notifications.py +120 -0
  633. wbcore/contrib/notifications/urls.py +26 -0
  634. wbcore/contrib/notifications/utils.py +20 -0
  635. wbcore/contrib/notifications/views.py +62 -0
  636. wbcore/contrib/notifications/viewsets/__init__.py +5 -0
  637. wbcore/contrib/notifications/viewsets/menus.py +14 -0
  638. wbcore/contrib/notifications/viewsets/notification_types.py +40 -0
  639. wbcore/contrib/notifications/viewsets/notifications.py +57 -0
  640. wbcore/contrib/tags/__init__.py +0 -0
  641. wbcore/contrib/tags/admin.py +17 -0
  642. wbcore/contrib/tags/apps.py +9 -0
  643. wbcore/contrib/tags/factories.py +27 -0
  644. wbcore/contrib/tags/filters.py +42 -0
  645. wbcore/contrib/tags/migrations/0001_initial.py +62 -0
  646. wbcore/contrib/tags/migrations/__init__.py +0 -0
  647. wbcore/contrib/tags/models/__init__.py +2 -0
  648. wbcore/contrib/tags/models/mixins.py +27 -0
  649. wbcore/contrib/tags/models/tags.py +103 -0
  650. wbcore/contrib/tags/release_notes/1_0_0.md +13 -0
  651. wbcore/contrib/tags/release_notes/__init__.py +0 -0
  652. wbcore/contrib/tags/serializers.py +87 -0
  653. wbcore/contrib/tags/signals.py +17 -0
  654. wbcore/contrib/tags/tests/__init__.py +0 -0
  655. wbcore/contrib/tags/tests/conftest.py +7 -0
  656. wbcore/contrib/tags/tests/tests.py +17 -0
  657. wbcore/contrib/tags/urls.py +14 -0
  658. wbcore/contrib/tags/viewsets/__init__.py +8 -0
  659. wbcore/contrib/tags/viewsets/display.py +50 -0
  660. wbcore/contrib/tags/viewsets/menu.py +15 -0
  661. wbcore/contrib/tags/viewsets/viewsets.py +58 -0
  662. wbcore/contrib/workflow/__init__.py +1 -0
  663. wbcore/contrib/workflow/admin/__init__.py +15 -0
  664. wbcore/contrib/workflow/admin/condition.py +8 -0
  665. wbcore/contrib/workflow/admin/data.py +14 -0
  666. wbcore/contrib/workflow/admin/display.py +8 -0
  667. wbcore/contrib/workflow/admin/process.py +36 -0
  668. wbcore/contrib/workflow/admin/step.py +91 -0
  669. wbcore/contrib/workflow/admin/transition.py +8 -0
  670. wbcore/contrib/workflow/admin/workflow.py +8 -0
  671. wbcore/contrib/workflow/apps.py +26 -0
  672. wbcore/contrib/workflow/configs.py +10 -0
  673. wbcore/contrib/workflow/decorators.py +25 -0
  674. wbcore/contrib/workflow/dispatch.py +23 -0
  675. wbcore/contrib/workflow/factories/__init__.py +18 -0
  676. wbcore/contrib/workflow/factories/condition.py +15 -0
  677. wbcore/contrib/workflow/factories/data.py +22 -0
  678. wbcore/contrib/workflow/factories/display.py +27 -0
  679. wbcore/contrib/workflow/factories/process.py +69 -0
  680. wbcore/contrib/workflow/factories/step.py +155 -0
  681. wbcore/contrib/workflow/factories/transition.py +25 -0
  682. wbcore/contrib/workflow/factories/workflow.py +19 -0
  683. wbcore/contrib/workflow/filters/__init__.py +24 -0
  684. wbcore/contrib/workflow/filters/condition.py +23 -0
  685. wbcore/contrib/workflow/filters/data.py +24 -0
  686. wbcore/contrib/workflow/filters/process.py +164 -0
  687. wbcore/contrib/workflow/filters/step.py +226 -0
  688. wbcore/contrib/workflow/filters/transition.py +41 -0
  689. wbcore/contrib/workflow/filters/workflow.py +43 -0
  690. wbcore/contrib/workflow/migrations/0001_initial.py +788 -0
  691. wbcore/contrib/workflow/migrations/0002_alter_step_step_type.py +31 -0
  692. wbcore/contrib/workflow/migrations/0003_alter_condition_attribute_name_and_more.py +75 -0
  693. wbcore/contrib/workflow/migrations/0004_alter_userstep_assignee_method.py +27 -0
  694. wbcore/contrib/workflow/migrations/0005_alter_userstep_assignee_method.py +17 -0
  695. wbcore/contrib/workflow/migrations/__init__.py +0 -0
  696. wbcore/contrib/workflow/models/__init__.py +19 -0
  697. wbcore/contrib/workflow/models/condition.py +123 -0
  698. wbcore/contrib/workflow/models/data.py +238 -0
  699. wbcore/contrib/workflow/models/display.py +33 -0
  700. wbcore/contrib/workflow/models/process.py +243 -0
  701. wbcore/contrib/workflow/models/step.py +735 -0
  702. wbcore/contrib/workflow/models/transition.py +70 -0
  703. wbcore/contrib/workflow/models/workflow.py +307 -0
  704. wbcore/contrib/workflow/serializers/__init__.py +37 -0
  705. wbcore/contrib/workflow/serializers/condition.py +64 -0
  706. wbcore/contrib/workflow/serializers/data.py +135 -0
  707. wbcore/contrib/workflow/serializers/display.py +25 -0
  708. wbcore/contrib/workflow/serializers/process.py +182 -0
  709. wbcore/contrib/workflow/serializers/signals.py +25 -0
  710. wbcore/contrib/workflow/serializers/step.py +364 -0
  711. wbcore/contrib/workflow/serializers/transition.py +80 -0
  712. wbcore/contrib/workflow/serializers/workflow.py +124 -0
  713. wbcore/contrib/workflow/sites.py +43 -0
  714. wbcore/contrib/workflow/tests/__init__.py +0 -0
  715. wbcore/contrib/workflow/tests/conftest.py +57 -0
  716. wbcore/contrib/workflow/tests/test_configs.py +6 -0
  717. wbcore/contrib/workflow/tests/test_dispatch.py +88 -0
  718. wbcore/contrib/workflow/tests/test_displays.py +119 -0
  719. wbcore/contrib/workflow/tests/test_filters.py +86 -0
  720. wbcore/contrib/workflow/tests/test_serializers.py +177 -0
  721. wbcore/contrib/workflow/tests/test_viewsets.py +358 -0
  722. wbcore/contrib/workflow/tests/test_workflow_assignees.py +225 -0
  723. wbcore/contrib/workflow/tests/tests.py +24 -0
  724. wbcore/contrib/workflow/urls.py +66 -0
  725. wbcore/contrib/workflow/utils.py +13 -0
  726. wbcore/contrib/workflow/viewsets/__init__.py +17 -0
  727. wbcore/contrib/workflow/viewsets/buttons/__init__.py +1 -0
  728. wbcore/contrib/workflow/viewsets/buttons/step.py +67 -0
  729. wbcore/contrib/workflow/viewsets/condition.py +32 -0
  730. wbcore/contrib/workflow/viewsets/data.py +33 -0
  731. wbcore/contrib/workflow/viewsets/display/__init__.py +15 -0
  732. wbcore/contrib/workflow/viewsets/display/condition.py +50 -0
  733. wbcore/contrib/workflow/viewsets/display/data.py +50 -0
  734. wbcore/contrib/workflow/viewsets/display/process.py +185 -0
  735. wbcore/contrib/workflow/viewsets/display/step.py +453 -0
  736. wbcore/contrib/workflow/viewsets/display/transition.py +75 -0
  737. wbcore/contrib/workflow/viewsets/display/workflow.py +167 -0
  738. wbcore/contrib/workflow/viewsets/endpoints/__init__.py +5 -0
  739. wbcore/contrib/workflow/viewsets/endpoints/condition.py +9 -0
  740. wbcore/contrib/workflow/viewsets/endpoints/data.py +9 -0
  741. wbcore/contrib/workflow/viewsets/endpoints/process.py +11 -0
  742. wbcore/contrib/workflow/viewsets/endpoints/step.py +24 -0
  743. wbcore/contrib/workflow/viewsets/endpoints/transition.py +18 -0
  744. wbcore/contrib/workflow/viewsets/menu/__init__.py +15 -0
  745. wbcore/contrib/workflow/viewsets/menu/condition.py +18 -0
  746. wbcore/contrib/workflow/viewsets/menu/data.py +18 -0
  747. wbcore/contrib/workflow/viewsets/menu/process.py +18 -0
  748. wbcore/contrib/workflow/viewsets/menu/step.py +123 -0
  749. wbcore/contrib/workflow/viewsets/menu/transition.py +18 -0
  750. wbcore/contrib/workflow/viewsets/menu/workflow.py +18 -0
  751. wbcore/contrib/workflow/viewsets/process.py +171 -0
  752. wbcore/contrib/workflow/viewsets/step.py +229 -0
  753. wbcore/contrib/workflow/viewsets/titles/__init__.py +16 -0
  754. wbcore/contrib/workflow/viewsets/titles/condition.py +13 -0
  755. wbcore/contrib/workflow/viewsets/titles/data.py +13 -0
  756. wbcore/contrib/workflow/viewsets/titles/process.py +26 -0
  757. wbcore/contrib/workflow/viewsets/titles/step.py +101 -0
  758. wbcore/contrib/workflow/viewsets/titles/transition.py +13 -0
  759. wbcore/contrib/workflow/viewsets/titles/workflow.py +13 -0
  760. wbcore/contrib/workflow/viewsets/transition.py +57 -0
  761. wbcore/contrib/workflow/viewsets/workflow.py +36 -0
  762. wbcore/contrib/workflow/workflows/__init__.py +1 -0
  763. wbcore/contrib/workflow/workflows/assignees.py +82 -0
  764. wbcore/crontab/__init__.py +0 -0
  765. wbcore/crontab/serializers.py +23 -0
  766. wbcore/crontab/viewsets.py +9 -0
  767. wbcore/dispatch.py +55 -0
  768. wbcore/docs/__init__.py +25 -0
  769. wbcore/docs/orderable.md +29 -0
  770. wbcore/docs/reparent.md +13 -0
  771. wbcore/dynamic_preferences_registry.py +29 -0
  772. wbcore/enums.py +98 -0
  773. wbcore/filters/__init__.py +21 -0
  774. wbcore/filters/backends.py +19 -0
  775. wbcore/filters/defaults.py +69 -0
  776. wbcore/filters/fields/__init__.py +15 -0
  777. wbcore/filters/fields/booleans.py +6 -0
  778. wbcore/filters/fields/choices.py +61 -0
  779. wbcore/filters/fields/content_type.py +33 -0
  780. wbcore/filters/fields/datetime.py +91 -0
  781. wbcore/filters/fields/models.py +113 -0
  782. wbcore/filters/fields/multiple_lookups.py +19 -0
  783. wbcore/filters/fields/numbers.py +46 -0
  784. wbcore/filters/fields/text.py +6 -0
  785. wbcore/filters/filterset.py +217 -0
  786. wbcore/filters/lookups.py +41 -0
  787. wbcore/filters/mixins.py +112 -0
  788. wbcore/filters/utils.py +21 -0
  789. wbcore/forms.py +125 -0
  790. wbcore/frontend.py +23 -0
  791. wbcore/frontend_user_configuration.py +96 -0
  792. wbcore/fsm/__init__.py +0 -0
  793. wbcore/fsm/markdown_extensions.py +31 -0
  794. wbcore/fsm/mixins.py +114 -0
  795. wbcore/management/__init__.py +88 -0
  796. wbcore/management/commands/__init__.py +0 -0
  797. wbcore/management/commands/bootstrap.py +18 -0
  798. wbcore/management/commands/clean_obsolete_object.py +21 -0
  799. wbcore/management/commands/handle_release_notes.py +53 -0
  800. wbcore/markdown/__init__.py +1 -0
  801. wbcore/markdown/admin.py +9 -0
  802. wbcore/markdown/dynamic_preferences_registry.py +16 -0
  803. wbcore/markdown/models.py +38 -0
  804. wbcore/markdown/template.py +38 -0
  805. wbcore/markdown/utils.py +36 -0
  806. wbcore/markdown/views.py +67 -0
  807. wbcore/menus/__init__.py +2 -0
  808. wbcore/menus/menus.py +96 -0
  809. wbcore/menus/registry.py +28 -0
  810. wbcore/menus/views.py +41 -0
  811. wbcore/messages.py +51 -0
  812. wbcore/metadata/__init__.py +0 -0
  813. wbcore/metadata/configs/__init__.py +0 -0
  814. wbcore/metadata/configs/base.py +86 -0
  815. wbcore/metadata/configs/buttons/__init__.py +4 -0
  816. wbcore/metadata/configs/buttons/bases.py +91 -0
  817. wbcore/metadata/configs/buttons/buttons.py +142 -0
  818. wbcore/metadata/configs/buttons/enums.py +25 -0
  819. wbcore/metadata/configs/buttons/metadata.py +8 -0
  820. wbcore/metadata/configs/buttons/view_config.py +176 -0
  821. wbcore/metadata/configs/display/__init__.py +4 -0
  822. wbcore/metadata/configs/display/configs.py +15 -0
  823. wbcore/metadata/configs/display/display.py +234 -0
  824. wbcore/metadata/configs/display/formatting.py +50 -0
  825. wbcore/metadata/configs/display/instance_display/__init__.py +15 -0
  826. wbcore/metadata/configs/display/instance_display/display.py +37 -0
  827. wbcore/metadata/configs/display/instance_display/enums.py +7 -0
  828. wbcore/metadata/configs/display/instance_display/layouts/__init__.py +3 -0
  829. wbcore/metadata/configs/display/instance_display/layouts/inlines.py +58 -0
  830. wbcore/metadata/configs/display/instance_display/layouts/layouts.py +77 -0
  831. wbcore/metadata/configs/display/instance_display/layouts/sections.py +43 -0
  832. wbcore/metadata/configs/display/instance_display/operators.py +22 -0
  833. wbcore/metadata/configs/display/instance_display/pages.py +40 -0
  834. wbcore/metadata/configs/display/instance_display/shortcuts.py +70 -0
  835. wbcore/metadata/configs/display/instance_display/signals.py +3 -0
  836. wbcore/metadata/configs/display/instance_display/styles.py +42 -0
  837. wbcore/metadata/configs/display/instance_display/utils.py +74 -0
  838. wbcore/metadata/configs/display/list_display.py +230 -0
  839. wbcore/metadata/configs/display/models.py +20 -0
  840. wbcore/metadata/configs/display/view_config.py +82 -0
  841. wbcore/metadata/configs/display/views.py +48 -0
  842. wbcore/metadata/configs/display/windows.py +28 -0
  843. wbcore/metadata/configs/documentations.py +11 -0
  844. wbcore/metadata/configs/endpoints.py +176 -0
  845. wbcore/metadata/configs/fields.py +17 -0
  846. wbcore/metadata/configs/filter_fields.py +42 -0
  847. wbcore/metadata/configs/identifiers.py +25 -0
  848. wbcore/metadata/configs/ordering_fields.py +24 -0
  849. wbcore/metadata/configs/paginations.py +11 -0
  850. wbcore/metadata/configs/preview.py +41 -0
  851. wbcore/metadata/configs/primary_keys.py +9 -0
  852. wbcore/metadata/configs/search_fields.py +9 -0
  853. wbcore/metadata/configs/titles.py +48 -0
  854. wbcore/metadata/configs/window_types.py +13 -0
  855. wbcore/metadata/exceptions.py +0 -0
  856. wbcore/metadata/metadata.py +33 -0
  857. wbcore/metadata/mixins.py +105 -0
  858. wbcore/metadata/tests/__init__.py +0 -0
  859. wbcore/metadata/tests/test_buttons.py +179 -0
  860. wbcore/metadata/utils.py +4 -0
  861. wbcore/migrations/0001_initial_squashed_squashed_0010_preset_appliedpreset.py +398 -0
  862. wbcore/migrations/0011_genericmodel.py +22 -0
  863. wbcore/migrations/0012_delete_notification.py +15 -0
  864. wbcore/migrations/0013_delete_colorgradient.py +14 -0
  865. wbcore/migrations/0014_biguserobjectpermission_system.py +44 -0
  866. wbcore/migrations/__init__.py +0 -0
  867. wbcore/models/__init__.py +6 -0
  868. wbcore/models/base.py +182 -0
  869. wbcore/models/fields.py +29 -0
  870. wbcore/models/orderable.py +6 -0
  871. wbcore/pagination.py +65 -0
  872. wbcore/pandas/__init__.py +0 -0
  873. wbcore/pandas/fields.py +136 -0
  874. wbcore/pandas/filters.py +113 -0
  875. wbcore/pandas/filterset.py +26 -0
  876. wbcore/pandas/metadata.py +14 -0
  877. wbcore/pandas/utils.py +129 -0
  878. wbcore/pandas/views.py +157 -0
  879. wbcore/permissions/__init__.py +0 -0
  880. wbcore/permissions/backend.py +35 -0
  881. wbcore/permissions/mixins.py +72 -0
  882. wbcore/permissions/permissions.py +50 -0
  883. wbcore/permissions/registry.py +32 -0
  884. wbcore/permissions/shortcuts.py +37 -0
  885. wbcore/permissions/utils.py +26 -0
  886. wbcore/release_notes/__init__.py +0 -0
  887. wbcore/release_notes/admin.py +30 -0
  888. wbcore/release_notes/buttons.py +25 -0
  889. wbcore/release_notes/display.py +57 -0
  890. wbcore/release_notes/filters.py +35 -0
  891. wbcore/release_notes/models.py +65 -0
  892. wbcore/release_notes/serializers.py +19 -0
  893. wbcore/release_notes/utils.py +14 -0
  894. wbcore/release_notes/viewsets.py +45 -0
  895. wbcore/reversion/__init__.py +0 -0
  896. wbcore/reversion/filters.py +38 -0
  897. wbcore/reversion/serializers.py +80 -0
  898. wbcore/reversion/viewsets/__init__.py +6 -0
  899. wbcore/reversion/viewsets/buttons.py +94 -0
  900. wbcore/reversion/viewsets/displays.py +44 -0
  901. wbcore/reversion/viewsets/endpoints.py +29 -0
  902. wbcore/reversion/viewsets/titles.py +17 -0
  903. wbcore/reversion/viewsets/viewsets.py +111 -0
  904. wbcore/routers.py +63 -0
  905. wbcore/search/__init__.py +62 -0
  906. wbcore/serializers/__init__.py +66 -0
  907. wbcore/serializers/fields/__init__.py +58 -0
  908. wbcore/serializers/fields/boolean.py +51 -0
  909. wbcore/serializers/fields/choice.py +36 -0
  910. wbcore/serializers/fields/datetime.py +143 -0
  911. wbcore/serializers/fields/fields.py +190 -0
  912. wbcore/serializers/fields/file.py +20 -0
  913. wbcore/serializers/fields/fsm.py +20 -0
  914. wbcore/serializers/fields/json.py +56 -0
  915. wbcore/serializers/fields/list.py +105 -0
  916. wbcore/serializers/fields/mixins.py +175 -0
  917. wbcore/serializers/fields/number.py +103 -0
  918. wbcore/serializers/fields/other.py +42 -0
  919. wbcore/serializers/fields/primary_key.py +22 -0
  920. wbcore/serializers/fields/related.py +152 -0
  921. wbcore/serializers/fields/text.py +136 -0
  922. wbcore/serializers/fields/types.py +41 -0
  923. wbcore/serializers/mixins.py +24 -0
  924. wbcore/serializers/serializers.py +415 -0
  925. wbcore/serializers/utils.py +142 -0
  926. wbcore/shares/__init__.py +1 -0
  927. wbcore/shares/config.py +63 -0
  928. wbcore/shares/decorator.py +13 -0
  929. wbcore/shares/signals.py +3 -0
  930. wbcore/shares/sites.py +28 -0
  931. wbcore/shares/views.py +24 -0
  932. wbcore/signals/__init__.py +6 -0
  933. wbcore/signals/filters.py +3 -0
  934. wbcore/signals/instance_buttons.py +5 -0
  935. wbcore/signals/merge.py +4 -0
  936. wbcore/signals/models.py +8 -0
  937. wbcore/signals/permissions.py +3 -0
  938. wbcore/signals/serializers.py +5 -0
  939. wbcore/tasks.py +83 -0
  940. wbcore/templates/reversion/__init__.py +0 -0
  941. wbcore/templates/reversion/compare_detail.html +19 -0
  942. wbcore/test/__init__.py +30 -0
  943. wbcore/test/mixins.py +709 -0
  944. wbcore/test/signals.py +6 -0
  945. wbcore/test/tests.py +131 -0
  946. wbcore/test/utils.py +227 -0
  947. wbcore/tests/__init__.py +0 -0
  948. wbcore/tests/conftest.py +55 -0
  949. wbcore/tests/e2e/__init__.py +0 -0
  950. wbcore/tests/e2e/test_e2e.py +24 -0
  951. wbcore/tests/models.py +6 -0
  952. wbcore/tests/test_cache/__init__.py +0 -0
  953. wbcore/tests/test_cache/test_decorators.py +30 -0
  954. wbcore/tests/test_cache/test_mixins.py +29 -0
  955. wbcore/tests/test_cache/test_registry.py +57 -0
  956. wbcore/tests/test_configs.py +65 -0
  957. wbcore/tests/test_enums.py +54 -0
  958. wbcore/tests/test_fields/__init__.py +0 -0
  959. wbcore/tests/test_fields/test_boolean_fields.py +48 -0
  960. wbcore/tests/test_fields/test_choice_fields.py +48 -0
  961. wbcore/tests/test_fields/test_datetime_fields.py +151 -0
  962. wbcore/tests/test_fields/test_fields.py +23 -0
  963. wbcore/tests/test_fields/test_file_fields.py +52 -0
  964. wbcore/tests/test_fields/test_json_fields.py +28 -0
  965. wbcore/tests/test_fields/test_list_fields.py +27 -0
  966. wbcore/tests/test_fields/test_mixins.py +111 -0
  967. wbcore/tests/test_fields/test_number_fields.py +190 -0
  968. wbcore/tests/test_fields/test_other_fields.py +61 -0
  969. wbcore/tests/test_fields/test_primary_key_fields.py +52 -0
  970. wbcore/tests/test_fields/test_related.py +80 -0
  971. wbcore/tests/test_fields/test_text_fields.py +117 -0
  972. wbcore/tests/test_filters/__init__.py +0 -0
  973. wbcore/tests/test_filters/test_mixins.py +108 -0
  974. wbcore/tests/test_filters/test_pandas.py +114 -0
  975. wbcore/tests/test_list_display.py +31 -0
  976. wbcore/tests/test_models/__init__.py +0 -0
  977. wbcore/tests/test_models/test_fields.py +25 -0
  978. wbcore/tests/test_models/test_mixins.py +31 -0
  979. wbcore/tests/test_new_display/__init__.py +0 -0
  980. wbcore/tests/test_new_display/test_inlines.py +13 -0
  981. wbcore/tests/test_new_display/test_layouts.py +15 -0
  982. wbcore/tests/test_new_display/test_operators.py +15 -0
  983. wbcore/tests/test_new_display/test_pages.py +8 -0
  984. wbcore/tests/test_new_display/test_sections.py +0 -0
  985. wbcore/tests/test_new_display/test_shortcuts.py +38 -0
  986. wbcore/tests/test_new_display/test_utils.py +48 -0
  987. wbcore/tests/test_pagination.py +31 -0
  988. wbcore/tests/test_serializers/__init__.py +0 -0
  989. wbcore/tests/test_serializers/test_fields.py +140 -0
  990. wbcore/tests/test_serializers/test_mixins.py +53 -0
  991. wbcore/tests/test_serializers/test_related.py +77 -0
  992. wbcore/tests/test_something.py +39 -0
  993. wbcore/tests/test_utils/__init__.py +0 -0
  994. wbcore/tests/test_utils/test_date.py +49 -0
  995. wbcore/tests/test_utils/test_date_builder.py +99 -0
  996. wbcore/tests/test_utils/test_primary.py +79 -0
  997. wbcore/tests/test_utils/test_signals.py +38 -0
  998. wbcore/tests/test_viewsets.py +20 -0
  999. wbcore/urls.py +114 -0
  1000. wbcore/utils/__init__.py +13 -0
  1001. wbcore/utils/cache.py +8 -0
  1002. wbcore/utils/date.py +220 -0
  1003. wbcore/utils/date_builder/__init__.py +18 -0
  1004. wbcore/utils/date_builder/components.py +42 -0
  1005. wbcore/utils/date_builder/offsets.py +27 -0
  1006. wbcore/utils/deprecations.py +11 -0
  1007. wbcore/utils/enum.py +23 -0
  1008. wbcore/utils/figures.py +290 -0
  1009. wbcore/utils/html.py +8 -0
  1010. wbcore/utils/importlib.py +13 -0
  1011. wbcore/utils/itertools.py +40 -0
  1012. wbcore/utils/models.py +275 -0
  1013. wbcore/utils/numbers.py +69 -0
  1014. wbcore/utils/prettytable.py +35 -0
  1015. wbcore/utils/print.py +29 -0
  1016. wbcore/utils/renderers.py +13 -0
  1017. wbcore/utils/rrules.py +37 -0
  1018. wbcore/utils/serializers.py +8 -0
  1019. wbcore/utils/settings.py +5 -0
  1020. wbcore/utils/signals.py +36 -0
  1021. wbcore/utils/string_loader.py +42 -0
  1022. wbcore/utils/strings.py +77 -0
  1023. wbcore/utils/task.py +6 -0
  1024. wbcore/utils/urls.py +70 -0
  1025. wbcore/utils/views.py +201 -0
  1026. wbcore/views.py +26 -0
  1027. wbcore/viewsets/__init__.py +10 -0
  1028. wbcore/viewsets/encoders.py +18 -0
  1029. wbcore/viewsets/generics.py +5 -0
  1030. wbcore/viewsets/mixins.py +290 -0
  1031. wbcore/viewsets/utils.py +26 -0
  1032. wbcore/viewsets/viewsets.py +142 -0
  1033. wbcore-2.2.1.dist-info/METADATA +62 -0
  1034. wbcore-2.2.1.dist-info/RECORD +1035 -0
  1035. wbcore-2.2.1.dist-info/WHEEL +5 -0
@@ -0,0 +1,1005 @@
1
+ import importlib
2
+ import json
3
+ import logging
4
+ import os
5
+ import sys
6
+ import traceback
7
+ import uuid
8
+ from contextlib import suppress
9
+ from copy import deepcopy
10
+ from datetime import datetime
11
+ from importlib import import_module
12
+ from typing import Any, Callable, Dict, Optional
13
+
14
+ import magic
15
+ from celery import chain, shared_task
16
+ from croniter import croniter
17
+ from django.apps import apps
18
+ from django.conf import settings
19
+ from django.contrib.contenttypes.fields import GenericForeignKey
20
+ from django.contrib.contenttypes.models import ContentType
21
+ from django.core.exceptions import ValidationError
22
+ from django.core.files.base import ContentFile
23
+ from django.db import models, transaction
24
+ from django.db.models import Q
25
+ from django.db.models.signals import post_delete, pre_delete
26
+ from django.db.utils import IntegrityError
27
+ from django.dispatch import receiver
28
+ from django.utils import timezone
29
+ from django.utils.functional import cached_property
30
+ from django.utils.translation import gettext_lazy as _
31
+ from django_celery_beat.models import CrontabSchedule, PeriodicTask, cronexp
32
+ from import_export.resources import Resource
33
+ from picklefield.fields import PickledObjectField
34
+ from tablib import Dataset
35
+ from tqdm import tqdm
36
+ from wbcore.contrib.notifications.dispatch import send_notification
37
+ from wbcore.contrib.notifications.utils import create_notification_type
38
+ from wbcore.utils.importlib import import_from_dotted_path
39
+ from wbcore.utils.models import ComplexToStringMixin
40
+
41
+ from .enums import ExportFormat, get_django_import_export_format
42
+ from .exceptions import ImportError
43
+
44
+ logger = logging.getLogger("io")
45
+
46
+
47
+ class ParserHandler(models.Model):
48
+ class MimeTypeChoices(models.TextChoices):
49
+ CSV = "text/csv", "CSV"
50
+ JSON = "application/json", "JSON"
51
+ XLS = "application/vnd.ms-excel", "XLS"
52
+ XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "XLSX"
53
+
54
+ parser = models.CharField(max_length=255)
55
+ handler = models.CharField(max_length=255)
56
+
57
+ allow_file_type = models.CharField(max_length=68, blank=True, null=True, choices=MimeTypeChoices.choices)
58
+
59
+ def __str__(self) -> str:
60
+ return f"{self.parser}::{self.handler}"
61
+
62
+ def parse(self, import_source: "ImportSource") -> Dict[str, Any]:
63
+ """
64
+ call the parser on the provided data
65
+ Args:
66
+ import_source: The import source containing the data
67
+ Returns:
68
+ The parsed data
69
+ """
70
+
71
+ parser = importlib.import_module(self.parser)
72
+ return parser.parse(import_source)
73
+
74
+ def handle(self, import_source: "ImportSource", parsed_data: Dict[str, Any], **kwargs):
75
+ """
76
+ Call the Handler on the parsed data
77
+ Args:
78
+ import_source: The initial import source
79
+ parsed_data: The parsed_data
80
+ """
81
+ if parsed_data:
82
+ model: Any = apps.get_model(self.handler)
83
+ if handler_class := getattr(model, "import_export_handler_class", None):
84
+ handler = handler_class(import_source)
85
+ handler.process(parsed_data, **kwargs)
86
+
87
+ class Meta:
88
+ unique_together = ("parser", "handler")
89
+ verbose_name = "Parser-Handler"
90
+ verbose_name_plural = "Parsers-Handlers"
91
+
92
+ @classmethod
93
+ def get_representation_value_key(self) -> str:
94
+ return "id"
95
+
96
+ @classmethod
97
+ def get_representation_label_key(self) -> str:
98
+ return "{{parser}}::{{handler}}"
99
+
100
+ @classmethod
101
+ def get_representation_endpoint(self) -> str:
102
+ return "wbcore:io:parserhandlerrepresentation-list"
103
+
104
+
105
+ class ImportedObjectProviderRelationship(ComplexToStringMixin, models.Model):
106
+ """
107
+ A model that represent the relationship/link between the imported object and a provider.
108
+
109
+ This model can be used to define different identifier in case the object is imported from difference providers
110
+ """
111
+
112
+ provider = models.ForeignKey(
113
+ to="io.Provider", related_name="imported_object_relationships", on_delete=models.CASCADE
114
+ )
115
+ content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
116
+ object_id = models.PositiveIntegerField()
117
+ content_object = GenericForeignKey("content_type", "object_id")
118
+ provider_identifier = models.CharField(max_length=255)
119
+
120
+ def compute_str(self) -> str:
121
+ try:
122
+ return f"{self.provider.title} -> {self.content_object}: {self.provider_identifier}"
123
+ except AttributeError:
124
+ return f"{self.provider.title} ({self.provider_identifier})"
125
+
126
+ class Meta:
127
+ verbose_name = "Content object Provider Identifier relationship"
128
+ verbose_name_plural = "Content object Provider Identifier relationships"
129
+ unique_together = [
130
+ ("content_type", "object_id", "provider"),
131
+ ("content_type", "provider_identifier", "provider"),
132
+ ]
133
+ indexes = [
134
+ models.Index(fields=["content_type", "object_id", "provider"]),
135
+ models.Index(fields=["content_type", "provider_identifier", "provider"]),
136
+ ]
137
+
138
+
139
+ class Provider(models.Model):
140
+ """
141
+ Represent a data vendor/provider. Every source is linked to a provider and are represented by a unique identifier
142
+ extracted from the data backend root folder
143
+
144
+ Example:
145
+ io/backend/refinitiv/instrument_prices.py => Provider.objects.get(key="refinitiv")
146
+ """
147
+
148
+ title = models.CharField(max_length=255)
149
+ key = models.CharField(max_length=255, unique=True)
150
+
151
+ def __str__(self) -> str:
152
+ return f"{self.title} ({self.key})"
153
+
154
+ class Meta:
155
+ verbose_name = "Provider"
156
+ verbose_name_plural = "Providers"
157
+
158
+
159
+ class DataBackend(models.Model):
160
+ """
161
+ Represents the instantiated backend imported through the specified dotted path and the passed parameters
162
+ """
163
+
164
+ title = models.CharField(max_length=255)
165
+
166
+ save_data_in_import_source = models.BooleanField(
167
+ default=True, help_text="If true, save the data in the import_source json field"
168
+ )
169
+ passive_only = models.BooleanField(
170
+ default=True, help_text="If True, this data backend is allowed to be called only from the import source."
171
+ )
172
+
173
+ backend_class_path = models.CharField(max_length=512)
174
+ backend_class_name = models.CharField(max_length=128, default="DataBackend")
175
+
176
+ provider = models.ForeignKey(
177
+ "io.Provider",
178
+ on_delete=models.SET_NULL,
179
+ null=True,
180
+ blank=True,
181
+ verbose_name=_("Provider"),
182
+ )
183
+
184
+ def get_internal_object(self, object_content_type: ContentType, provider_external_id: str) -> models.Model | None:
185
+ """
186
+ Internal facing function that returns the internal id of the object with the given external provider identifier. If the relationship already exists, we simply returns the stored id. Otherwise we call the appropriate function to get
187
+ Args:
188
+ object_content_type: ContentType object representing the seeked object id
189
+ provider_external_id: The object identifier as given by this backend provider
190
+
191
+ Returns:
192
+ The object as stored in the importer object-provider relationship. None if it doesn't exist yet
193
+ """
194
+ if rel := ImportedObjectProviderRelationship.objects.filter(
195
+ provider=self.provider, provider_identifier=provider_external_id, content_type=object_content_type
196
+ ).first():
197
+ return rel.content_object
198
+
199
+ class Meta:
200
+ verbose_name = "Data Backend"
201
+ verbose_name_plural = "Data Backends"
202
+
203
+ @cached_property
204
+ def backend_class(self) -> Any:
205
+ """
206
+ Return the imported backend class
207
+ Returns:
208
+ The backend class
209
+ """
210
+ return getattr(import_module(self.backend_class_path), self.backend_class_name)
211
+
212
+ def __str__(self) -> str:
213
+ return self.title
214
+
215
+ @classmethod
216
+ def get_representation_value_key(self) -> str:
217
+ return "id"
218
+
219
+ @classmethod
220
+ def get_representation_label_key(self) -> str:
221
+ return "{{title}}"
222
+
223
+ @classmethod
224
+ def get_representation_endpoint(self) -> str:
225
+ return "wbcore:io:databackendrepresentation-list"
226
+
227
+
228
+ class Source(models.Model):
229
+ title = models.CharField(max_length=128)
230
+ uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
231
+ parser_handler = models.ManyToManyField(
232
+ "io.ParserHandler",
233
+ verbose_name="Parser/Handler",
234
+ related_name="sources",
235
+ )
236
+
237
+ is_active = models.BooleanField(default=True)
238
+
239
+ connection_parameters = models.JSONField(default=dict, null=True, blank=True)
240
+ import_parameters = models.JSONField(default=dict, null=True, blank=True)
241
+
242
+ credentials = models.ManyToManyField("io.ImportCredential", blank=True, related_name="sources")
243
+
244
+ data_backend = models.ForeignKey("io.DataBackend", on_delete=models.CASCADE, verbose_name=_("Data Backend"))
245
+
246
+ crontab = models.ForeignKey(
247
+ CrontabSchedule,
248
+ on_delete=models.SET_NULL,
249
+ null=True,
250
+ blank=True,
251
+ verbose_name=_("Crontab Schedule"),
252
+ help_text=_("Crontab Schedule to run the task on. " "Set only one schedule type, leave the others null."),
253
+ )
254
+
255
+ periodic_task = models.ForeignKey(
256
+ PeriodicTask,
257
+ on_delete=models.SET_NULL,
258
+ null=True,
259
+ blank=True,
260
+ verbose_name="Periodic Task",
261
+ related_name="sources",
262
+ )
263
+
264
+ import_timedelta_interval = models.IntegerField(default=0)
265
+
266
+ def __init__(self, *args, **kwargs):
267
+ super().__init__(*args, **kwargs)
268
+ self.instantiated_backend: Any = None
269
+
270
+ def init_backend(self, execution_datetime: datetime):
271
+ """
272
+ Singleton that instantiate the backend attribute
273
+
274
+ Args:
275
+ execution_datetime: Datetime at which the backend is supposed to be initialize
276
+
277
+ """
278
+ if not self.instantiated_backend:
279
+ self.instantiated_backend = self.data_backend.backend_class(
280
+ import_credential=self.get_valid_credential(execution_datetime),
281
+ data_backend=self.data_backend,
282
+ **self.connection_parameters,
283
+ )
284
+
285
+ def save(self, *args, **kwargs):
286
+ super().save(*args, **kwargs)
287
+ self.update_periodic_task()
288
+
289
+ def __str__(self) -> str:
290
+ return self.title if self.title else f"{self.uuid}"
291
+
292
+ class Meta:
293
+ verbose_name = "Source"
294
+ verbose_name_plural = "Sources"
295
+
296
+ @property
297
+ def current_valid_credential(self) -> "ImportCredential":
298
+ """
299
+ Property that returns the current valid credential
300
+
301
+ Returns:
302
+ The valid ImportCredential (as of now)
303
+ """
304
+ return self.get_valid_credential(timezone.now())
305
+
306
+ @property
307
+ def crontab_repr(self) -> str:
308
+ """
309
+ Returns the crontab string representation.
310
+ :return: crontab string representation
311
+ """
312
+ if not self.crontab:
313
+ return ""
314
+
315
+ return "{0} {1} {2} {3} {4}".format(
316
+ cronexp(self.crontab.minute),
317
+ cronexp(self.crontab.hour),
318
+ cronexp(self.crontab.day_of_month),
319
+ cronexp(self.crontab.month_of_year),
320
+ cronexp(self.crontab.day_of_week),
321
+ )
322
+
323
+ def update_periodic_task(self):
324
+ """
325
+ Utility function to find and update the link periodic task given the current source state
326
+ """
327
+ if self.crontab:
328
+ if task := self.periodic_task:
329
+ task.crontab = self.crontab
330
+ task.enabled = self.is_active
331
+ task.save()
332
+ else:
333
+ task = PeriodicTask.objects.update_or_create(
334
+ name=f"Import-Export: {self}",
335
+ defaults={
336
+ "crontab": self.crontab,
337
+ "args": json.dumps([self.pk]),
338
+ "task": "wbcore.contrib.io.models.trigger_workflow_as_task",
339
+ },
340
+ )[0]
341
+ Source.objects.filter(pk=self.pk).update(periodic_task=task)
342
+ self.refresh_from_db()
343
+
344
+ def get_valid_credential(self, val_datetime: datetime) -> "ImportCredential":
345
+ """
346
+ Get the valid credential
347
+ Args:
348
+ val_datetime: The date at which the credentials need to be valid
349
+ Returns:
350
+ A valid credential
351
+ """
352
+ return (
353
+ self.credentials.filter(
354
+ (Q(validity_start__lte=val_datetime) | Q(validity_start__isnull=True))
355
+ & (Q(validity_end__gte=val_datetime) | Q(validity_end__isnull=True))
356
+ )
357
+ .order_by("-validity_end")
358
+ .first()
359
+ )
360
+
361
+ def get_or_create_provider_id(self, obj: models.Model) -> str | None:
362
+ """
363
+ Workflow method that try to get or fetch the provider identifier for the passed object.
364
+
365
+ If the relationship already exists, we directly returns the stored external id. Otherwise we call the appropriate function on the already instantiated backend to get it from the provider itself. If the value is returned, we store the relationship
366
+
367
+ Args:
368
+ obj: The object to get the external provider identifier from
369
+
370
+ Returns:
371
+ If it exists, the external identifier (from this source provider). None otherwise.
372
+ """
373
+
374
+ if (provider := self.data_backend.provider) and self.instantiated_backend is not None:
375
+ if self.instantiated_backend.is_object_valid(obj):
376
+ if rel := ImportedObjectProviderRelationship.objects.filter(
377
+ provider=provider, content_type=ContentType.objects.get_for_model(obj), object_id=obj.pk
378
+ ).first():
379
+ return rel.provider_identifier
380
+ else:
381
+ if provider_identifier := self.instantiated_backend.get_provider_id(obj):
382
+ with transaction.atomic():
383
+ ImportedObjectProviderRelationship.objects.create(
384
+ provider=provider,
385
+ content_type=ContentType.objects.get_for_model(obj),
386
+ object_id=obj.pk,
387
+ provider_identifier=provider_identifier,
388
+ )
389
+ return provider_identifier
390
+ obj.save()
391
+ else:
392
+ raise ValueError(
393
+ f"You can't create a provider relationship with {obj} with for backend {self.data_backend} without a provider assigned"
394
+ )
395
+
396
+ def is_valid_date(self, sync_datetime: datetime) -> bool:
397
+ """
398
+ check wether a date is valid given the stored crontab schedule
399
+ Args:
400
+ sync_datetime: The datetime at which validity needs to be checked
401
+ Returns:
402
+ True if the given date is valid given the source crontab
403
+ """
404
+ if not self.crontab:
405
+ return False
406
+ return croniter.match(self.crontab_repr, sync_datetime)
407
+
408
+ def trigger_workflow(
409
+ self,
410
+ execution_time: Optional[datetime] = None,
411
+ callback_signature: Callable | None = None,
412
+ callback_args: list | None = None,
413
+ callback_kwargs: Dict | None = None,
414
+ synchronous: bool = False,
415
+ **kwargs,
416
+ ):
417
+ """
418
+ The entry point function of the whole import source workflow.
419
+ Loop over all files return by the attached backend and save the result into a serie of import sources.
420
+
421
+ Args:
422
+ execution_time: The time at which the worfklow was triggerd
423
+ callback_signature: The celery canvas signature to be called upon completion (if any)
424
+ callback_args: the potential positional arguments to be passed to the callback function (if any)
425
+ callback_kwargs: Potential keyword arguments to be passed to the callback function (if any)
426
+ synchronous: If true, will execute the workflow synchronously. Default to False
427
+ **kwargs: keyword arguments to be passed down along the workflow
428
+
429
+ Returns:
430
+ A generator of import sources
431
+ """
432
+ if not execution_time:
433
+ execution_time = timezone.now()
434
+ # If a queryset is provided, we need to convert it into a list of valid external id that we will get from the relationship through model or from the backend directly if not existing
435
+
436
+ chained_tasks = [
437
+ generate_import_sources_as_task.s(self.pk, execution_time, **kwargs),
438
+ process_import_sources_as_task.s(),
439
+ ]
440
+ if not callback_args:
441
+ callback_args = []
442
+ if not callback_kwargs:
443
+ callback_kwargs = {}
444
+ if callback_signature:
445
+ chained_tasks.append(callback_signature.si(*callback_args, **callback_kwargs))
446
+ if synchronous:
447
+ import_source_ids = chained_tasks[0].apply().get()
448
+ chained_tasks[1].apply((import_source_ids,))
449
+ if len(chained_tasks) > 2:
450
+ chained_tasks[2].apply(tuple(callback_args), callback_kwargs)
451
+ else:
452
+ chain(*chained_tasks).apply_async(eta=execution_time)
453
+
454
+ def generate_import_sources(
455
+ self,
456
+ execution_time: datetime,
457
+ only_handler: str | None = None,
458
+ **kwargs,
459
+ ) -> list["ImportSource"]:
460
+ """
461
+ Process the source based on its attached backend (returns an error if such backend does not exist)
462
+ The backend is first initialize through constructor and then, The generator `get_files` is called, passing the
463
+ execution time, the expected data time, and some keyword arguments.
464
+
465
+ For each file returns by the generator, we create a ImportSource objects and trigger its import task
466
+ Args:
467
+ execution_time: The time at which this task was triggered
468
+ **kwargs: keyword arguments to be passed down along the workflow
469
+ Returns:
470
+ Return a generator of import sources
471
+ """
472
+ if not self.data_backend:
473
+ raise ValueError("Data Backend not specied for this source")
474
+
475
+ # Import the data backend module and extract its class_name
476
+ res = []
477
+ # Get the expected date at which this source should return its data (e.g. T-1)
478
+
479
+ # Loop over the backend returned generator
480
+ self.init_backend(execution_time)
481
+ errors = []
482
+ queryset = kwargs.pop("queryset", self.instantiated_backend.get_default_queryset())
483
+ if self.data_backend.provider and queryset is not None:
484
+ obj_external_ids = []
485
+ for obj in tqdm(queryset, total=queryset.count()):
486
+ try:
487
+ if external_id := self.get_or_create_provider_id(obj):
488
+ obj_external_ids.append(external_id)
489
+ except IntegrityError:
490
+ errors.append(str(obj))
491
+ kwargs["obj_external_ids"] = obj_external_ids
492
+ else:
493
+ kwargs["queryset"] = queryset
494
+ streams = self.instantiated_backend.get_files(
495
+ execution_time,
496
+ **{
497
+ **self.import_parameters,
498
+ **kwargs,
499
+ },
500
+ )
501
+ if streams:
502
+ for file_name, _file in streams:
503
+ content_file = ContentFile(_file.getvalue())
504
+ content_file.name = file_name
505
+ parser_handlers = self.parser_handler.all()
506
+ if only_handler:
507
+ parser_handlers = parser_handlers.filter(handler__iexact=only_handler)
508
+ for parser_handler in parser_handlers:
509
+ with transaction.atomic():
510
+ import_source = ImportSource.objects.create(
511
+ file=content_file,
512
+ parser_handler=parser_handler,
513
+ save_data=self.data_backend.save_data_in_import_source,
514
+ source=self,
515
+ )
516
+ res.append(import_source)
517
+ return res
518
+
519
+ @classmethod
520
+ def load_sources_from_settings(self, settings: list[tuple[list[tuple[str, str]], str, dict[str, Any]]]):
521
+ """
522
+ Utility classmethod to parser sources from the settings.
523
+
524
+ We assume data structure as follow
525
+
526
+ [
527
+ (
528
+ [("module1.HandlerModel1", "module1.io.parsers.provider1.handler1")],
529
+ "module1.io.backends.provider1.handler1.DataBackend",
530
+ {
531
+ "credentials": [credential_dict],
532
+ "crontab": "10 9,17,1 * * *",
533
+ },
534
+ ),
535
+ (
536
+ [("module2.HandlerModel2", "module2.io.parsers.provider2.handler2")],
537
+ "module2.io.backends.provider2.handler2.DataBackend",
538
+ )
539
+ ]
540
+ Args:
541
+ settings: The source settings to parse
542
+
543
+ """
544
+ for parser_handlers, data_backend_module, config in settings:
545
+ with suppress(Exception):
546
+ # This method is loaded when server boots. If this fail, it can prevent the server from starting.
547
+ backend_class_path, _, backend_class_name = data_backend_module.rpartition(".")
548
+
549
+ data_backend = DataBackend.objects.get(
550
+ backend_class_path=backend_class_path, backend_class_name=backend_class_name
551
+ )
552
+ crontab = None
553
+ credentials = config.pop("credentials", [])
554
+ if crontab_data := config.pop("crontab", None):
555
+ minute, hour, day_of_month, month_of_year, day_of_week = crontab_data.split(" ")
556
+ crontab_kwargs = {
557
+ "minute": minute,
558
+ "hour": hour,
559
+ "day_of_week": day_of_week,
560
+ "day_of_month": day_of_month,
561
+ "month_of_year": month_of_year,
562
+ }
563
+ crontabs = CrontabSchedule.objects.filter(**crontab_kwargs)
564
+ if crontabs.exists():
565
+ crontab = crontabs.first()
566
+ else:
567
+ crontab = CrontabSchedule.objects.create(**crontab_kwargs)
568
+
569
+ source, source_created = Source.objects.get_or_create(
570
+ data_backend=data_backend,
571
+ defaults={"crontab": crontab, **config},
572
+ )
573
+ for handler, parser in parser_handlers:
574
+ parser_handler = ParserHandler.objects.get_or_create(parser=parser, handler=handler)[0]
575
+ if parser_handler not in source.parser_handler.all():
576
+ source.parser_handler.add(parser_handler)
577
+ for credential_data in credentials:
578
+ credential, created = ImportCredential.objects.get_or_create(
579
+ key=credential_data["key"], defaults=credential_data
580
+ )
581
+ if credential not in source.credentials.all():
582
+ source.credentials.add(credential)
583
+ source.save()
584
+
585
+ @classmethod
586
+ def get_representation_value_key(self) -> str:
587
+ return "id"
588
+
589
+ @classmethod
590
+ def get_representation_label_key(self) -> str:
591
+ return "{{title}} ({{id}})"
592
+
593
+ @classmethod
594
+ def get_representation_endpoint(self) -> str:
595
+ return "wbcore:io:sourcerepresentation-list"
596
+
597
+
598
+ @receiver(post_delete, sender="io.Source")
599
+ def post_delete_source(sender, instance, **kwargs):
600
+ # Delete the attached period task
601
+ if instance.periodic_task:
602
+ instance.periodic_task.delete()
603
+
604
+
605
+ @receiver(pre_delete, sender="io.Source")
606
+ def pre_delete_source(sender, instance, **kwargs):
607
+ # Unassign import source from the deleting source
608
+ ImportSource.objects.filter(source=instance).update(source=None)
609
+
610
+
611
+ @shared_task(queue="importexport")
612
+ def trigger_workflow_as_task(source_id: int, execution_time: datetime | None = None, **kwargs):
613
+ """
614
+ Call the `import_source` as a celery task
615
+ """
616
+ source = Source.objects.get(id=source_id)
617
+ source.trigger_workflow(execution_time=execution_time, **kwargs)
618
+
619
+
620
+ @shared_task(queue="importexport")
621
+ def generate_import_sources_as_task(source_id: int, execution_time: datetime, **kwargs) -> list[int]:
622
+ """
623
+ Call the `import_source` as a celery task
624
+ """
625
+ source = Source.objects.get(id=source_id)
626
+ import_source_ids = [
627
+ import_source.pk for import_source in source.generate_import_sources(execution_time, **kwargs)
628
+ ] # convert to list to be json serializable
629
+ return import_source_ids
630
+
631
+
632
+ @shared_task(queue="importexport")
633
+ def process_import_sources_as_task(import_source_ids: list[int]):
634
+ """
635
+ Call the `import_source` as a celery task
636
+ """
637
+ errors = []
638
+ for import_source_id in import_source_ids:
639
+ import_source = ImportSource.objects.get(id=import_source_id)
640
+ try:
641
+ import_source.import_data(force_reimport=True)
642
+ except ImportError:
643
+ errors.append(import_source_id)
644
+ if len(errors) > 0:
645
+ raise ImportError(f"Error while processing import source {errors}")
646
+
647
+
648
+ class ImportExportSource(models.Model):
649
+ class Status(models.TextChoices):
650
+ PENDING = "PENDING", "Pending"
651
+ PROCESSED = "PROCESSED", "Processed"
652
+ WARNING = "WARNING", "Warning"
653
+ ERROR = "ERROR", "Error"
654
+ IGNORED = "IGNORED", "Ignore"
655
+
656
+ created = models.DateTimeField(auto_now_add=True)
657
+ last_updated = models.DateTimeField(auto_now=True)
658
+
659
+ status = models.CharField(
660
+ max_length=16,
661
+ choices=Status.choices,
662
+ default=Status.PENDING.value,
663
+ )
664
+
665
+ log = models.TextField(null=True, blank=True)
666
+ origin = models.CharField(max_length=255, null=True, blank=True)
667
+ creator = models.ForeignKey(
668
+ "authentication.User", null=True, blank=True, verbose_name="Creator", on_delete=models.SET_NULL
669
+ )
670
+ data = models.JSONField(default=dict, null=True, blank=True)
671
+
672
+ class Meta:
673
+ abstract = True
674
+
675
+
676
+ class ExportSource(ImportExportSource):
677
+ file = models.FileField(max_length=256, upload_to="io/export_source/files", null=True, blank=True)
678
+ format = models.IntegerField(
679
+ choices=ExportFormat.choices,
680
+ default=ExportFormat.CSV.value,
681
+ verbose_name=_("Format of file to be exported"),
682
+ )
683
+
684
+ content_type = models.ForeignKey(ContentType, verbose_name=_("Export job Content Type"), on_delete=models.CASCADE)
685
+
686
+ resource_path = models.CharField(
687
+ verbose_name=_("Resource path to use when exporting"), max_length=255, default="", blank=True, null=True
688
+ )
689
+
690
+ resource_kwargs = models.JSONField(default=dict, blank=True, null=False)
691
+
692
+ query_str = models.TextField(verbose_name=_("SQL query to be executed"), blank=True, null=False)
693
+ query_params = PickledObjectField( # we have to use picklefield because the sql parameters are given as python object. However, no django model should be stored in this field, which should be the case as these objects are given through their IDs
694
+ blank=True, verbose_name=_("SQL query parameters to be used with the sql query"), default=list
695
+ )
696
+
697
+ class Meta:
698
+ verbose_name = _("Export Source")
699
+ verbose_name_plural = _("Export Sources")
700
+ notification_types = [
701
+ create_notification_type(
702
+ code="io.export_done",
703
+ title="File Export has finished",
704
+ help_text="Notifies user when their submitted export files is done and available",
705
+ web=True,
706
+ mobile=True,
707
+ email=True,
708
+ )
709
+ ]
710
+ constraints = [
711
+ models.CheckConstraint(
712
+ check=Q(query_str__isnull=False, resource_path__isnull=False) | ~Q(data__exact=dict()),
713
+ name="check_either_data_or_resource_isnotnull",
714
+ )
715
+ ]
716
+
717
+ @property
718
+ def file_format(self):
719
+ return get_django_import_export_format(self.format)()
720
+
721
+ @property
722
+ def resource(self) -> Resource:
723
+ """
724
+ Load into an attribute the instantiated resource loaded from the resource path
725
+ """
726
+ resource_class = import_from_dotted_path(self.resource_path)
727
+ return resource_class(**self.resource_kwargs)
728
+
729
+ @property
730
+ def queryset(self) -> models.QuerySet:
731
+ """
732
+ Recreate the base queryset from the content type model class base manager and the saved query string and parameters
733
+ """
734
+
735
+ return self.content_type.model_class().objects.raw(self.query_str, self.query_params)
736
+
737
+ def get_export_filename(self):
738
+ date_str = timezone.now().strftime("%Y-%m-%d")
739
+ ts = datetime.timestamp(timezone.now())
740
+ filename = "%s-%s_%s.%s" % (
741
+ self.content_type.model_class().__name__,
742
+ date_str,
743
+ ts,
744
+ self.file_format.get_extension(),
745
+ )
746
+ return filename
747
+
748
+ def notify(self):
749
+ """
750
+ Notify the user if the export job is done and accessible
751
+ """
752
+ if self.file and self.status == self.Status.PROCESSED:
753
+ send_notification(
754
+ code="io.export_done",
755
+ title=_("Your export file is available"),
756
+ body=_("<p>The export job you requested is finished and available for one hour.</p>").format(
757
+ url=self.file.url
758
+ ),
759
+ endpoint=self.file.url,
760
+ user=self.creator,
761
+ )
762
+
763
+ def export_data(self, **kwargs):
764
+ self.log = ""
765
+ self.status = self.Status.PENDING.name
766
+ self.save()
767
+ try:
768
+ file_format = get_django_import_export_format(self.format)()
769
+ if self.data:
770
+ # Data was saved as a dictionary from a tablib.dataset, we need to recreate it
771
+ data = Dataset()
772
+ data.headers = self.data["headers"]
773
+ data.extend(self.data["data"])
774
+ else:
775
+ data = self.resource.export(queryset=self.queryset)
776
+
777
+ export_data = file_format.export_data(data)
778
+ if not file_format.is_binary():
779
+ export_data = export_data.encode("utf-8")
780
+ self.file.save(self.get_export_filename(), ContentFile(export_data))
781
+ self.status = self.Status.PROCESSED.name
782
+ self.save()
783
+ self.notify()
784
+
785
+ except Exception:
786
+ ex_type, ex_value, _ = sys.exc_info()
787
+ self.status = self.Status.ERROR.name
788
+ self.log = f"{ex_type}: {ex_value}\n"
789
+ self.log += traceback.format_exc()
790
+ self.save()
791
+
792
+
793
+ class ImportSource(ImportExportSource):
794
+ file = models.FileField(max_length=256, upload_to="io/import_source/files", null=True, blank=True)
795
+
796
+ save_data = models.BooleanField(default=True, help_text="If True, will save the raw data in a field")
797
+ progress_index = models.PositiveIntegerField(default=0)
798
+
799
+ parser_handler = models.ForeignKey("io.ParserHandler", on_delete=models.CASCADE)
800
+ source = models.ForeignKey("io.Source", null=True, blank=True, verbose_name="Source", on_delete=models.SET_NULL)
801
+ errors_log = models.TextField(null=True, blank=True)
802
+
803
+ class Meta:
804
+ verbose_name = "Import Source"
805
+ verbose_name_plural = "Import Sources"
806
+ notification_types = [
807
+ create_notification_type(
808
+ code="io.import_done",
809
+ title="File Import has finished",
810
+ help_text="Notifies user when their submitted import files has finished",
811
+ )
812
+ ]
813
+
814
+ def __str__(self) -> str:
815
+ return f"{self.pk}"
816
+
817
+ def _validate_file_type(self):
818
+ """
819
+ Valid the file type based on the allowed types specified by the parser
820
+ Returns:
821
+ True if the filetype is valid
822
+ """
823
+ if (upload := self.file) and (allow_file_type := self.parser_handler.allow_file_type):
824
+ file_type = magic.from_buffer(upload.file.read(1024), mime=True)
825
+ if file_type != allow_file_type:
826
+ raise ValidationError("File type not supported.")
827
+
828
+ def save(self, *args, **kwargs):
829
+ # Check here company name, modify ...
830
+ self._validate_file_type()
831
+ super().save(*args, **kwargs)
832
+
833
+ def _parse_data(self) -> dict[str, Any]:
834
+ """
835
+ Given the ParserHandler linked object, we import its related module and call its defined `parse` function
836
+ Returns:
837
+ Dict The parsed data as a python dictionary
838
+ """
839
+ if not self.file:
840
+ raise ValueError("This import source does not include a valid file")
841
+ parsed_data = self.parser_handler.parse(self)
842
+ if self.save_data:
843
+ self.data = parsed_data
844
+ self.save()
845
+ return parsed_data
846
+
847
+ def _process_data(self, parsed_data: dict[str, Any], **kwargs):
848
+ """
849
+ Given the parsed data (as a python dictionary), import the corresponding handler and calls its defined `process
850
+ Args:
851
+ data: The deserialized data to be passed down to the parser
852
+ """
853
+ self.status = self.Status.PROCESSED.name
854
+ if parsed_data:
855
+ parsed_data_copy = deepcopy(parsed_data)
856
+ if data := parsed_data_copy.pop("data", None):
857
+ self.parser_handler.handle(self, {"data": data[self.progress_index :], **parsed_data_copy}, **kwargs)
858
+ if self.errors_log:
859
+ self.status = self.Status.WARNING.name
860
+ self.save()
861
+
862
+ def import_data(self, force_reimport: Optional[bool] = False, **kwargs):
863
+ """
864
+ General workflow method:
865
+ * Parse the data given the linked import file
866
+ * Handle the parsed data
867
+ * Change the import_source status based on sucess
868
+ Args:
869
+ silent: False if this method is not suppose to silently catch the exceptions
870
+ """
871
+ if force_reimport:
872
+ self.progress_index = 0
873
+ self.log = ""
874
+ self.errors_log = ""
875
+ # self.data = {}
876
+ self.status = self.Status.PENDING.name
877
+ self.save()
878
+ debug = kwargs.get("debug", settings.DEBUG)
879
+ try:
880
+ data = self._parse_data()
881
+ self._process_data(data, debug=debug)
882
+ except Exception as e:
883
+ ex_type, ex_value, _ = sys.exc_info()
884
+ self.status = self.Status.ERROR.name
885
+ self.log = f"{ex_type}: {ex_value}\n"
886
+ self.log += traceback.format_exc()
887
+ self.save()
888
+ if not debug:
889
+ raise ImportError(
890
+ f"Could not import file {self.file.name} for backend {self.source.data_backend.title} and parser/handler {self.parser_handler}"
891
+ )
892
+ else:
893
+ raise e
894
+
895
+ @classmethod
896
+ def get_representation_value_key(self) -> str:
897
+ return "id"
898
+
899
+ @classmethod
900
+ def get_representation_label_key(self) -> str:
901
+ return "{{file}}"
902
+
903
+ @classmethod
904
+ def get_representation_endpoint(self) -> str:
905
+ return "wbcore:io:importsourcerepresentation-list"
906
+
907
+
908
+ @shared_task(queue="importexport")
909
+ def import_data_as_task(import_source_id: int, **kwargs):
910
+ """
911
+ Call `import_data` as a celery task
912
+ :param Int The import_source id
913
+ """
914
+ import_source = ImportSource.objects.get(id=import_source_id)
915
+ import_source.import_data(**kwargs)
916
+
917
+
918
+ @shared_task(queue="importexport")
919
+ def export_data_as_task(export_source_id: int, **kwargs):
920
+ """
921
+ Call `import_data` as a celery task
922
+ :param Int The import_source id
923
+ """
924
+ export_source = ExportSource.objects.get(id=export_source_id)
925
+ export_source.export_data(**kwargs)
926
+
927
+
928
+ @receiver(models.signals.post_save, sender=ExportSource)
929
+ def post_save_export_source(sender, instance: ExportSource, created: bool, raw: bool, **kwargs):
930
+ """Triggers the export task on creation"""
931
+
932
+ if not raw and created and instance.status == ExportSource.Status.PENDING:
933
+ transaction.on_commit(lambda: export_data_as_task.delay(instance.id))
934
+
935
+
936
+ def validate_key_file(value: models.FileField):
937
+ ext = os.path.splitext(value.name)[1]
938
+ if ext.lower() != ".key":
939
+ raise ValidationError("The file extension needs to be a .key")
940
+
941
+
942
+ def validate_pem_file(value: models.FileField):
943
+ ext = os.path.splitext(value.name)[1]
944
+ if ext.lower() != ".pem":
945
+ raise ValidationError("The file extension needs to be a .pem")
946
+
947
+
948
+ class ImportCredential(models.Model):
949
+ class Type(models.TextChoices):
950
+ CREDENTIAL = "CREDENTIAL", "Credential"
951
+ AUTHENTICATION_TOKEN = "AUTHENTICATION_TOKEN", "Authentication Token"
952
+ CERTIFICATE = "CERTIFICATE", "Certificate"
953
+
954
+ key = models.CharField(max_length=255)
955
+ type = models.CharField(choices=Type.choices, default=Type.CREDENTIAL, max_length=64)
956
+
957
+ username = models.CharField(max_length=255, null=True, blank=True)
958
+ password = models.CharField(max_length=255, null=True, blank=True)
959
+
960
+ authentication_token = models.CharField(max_length=2048, null=True, blank=True)
961
+
962
+ certificate_pem = models.FileField(
963
+ max_length=256,
964
+ null=True,
965
+ blank=True,
966
+ upload_to="io/import_credential/certificates",
967
+ help_text="We are expecting a .cert file",
968
+ validators=[validate_pem_file],
969
+ )
970
+ certificate_key = models.FileField(
971
+ max_length=256,
972
+ null=True,
973
+ blank=True,
974
+ upload_to="io/import_credential/certificates",
975
+ help_text="We are expecting a .key file",
976
+ validators=[validate_key_file],
977
+ )
978
+
979
+ additional_resources = models.JSONField(default=dict, null=True, blank=True)
980
+
981
+ validity_start = models.DateTimeField(null=True, blank=True)
982
+ validity_end = models.DateTimeField(null=True, blank=True)
983
+
984
+ def save(self, *args, **kwargs):
985
+ if self.type == self.Type.CREDENTIAL and (not self.username or not self.password):
986
+ raise ValidationError("The type is credential, you need to specify a username and a password")
987
+ elif self.type == self.Type.AUTHENTICATION_TOKEN and not self.authentication_token:
988
+ raise ValidationError("The type is authentication header, you need to specify a valid header")
989
+ elif self.type == self.Type.CERTIFICATE and (not self.certificate_key or not self.certificate_pem):
990
+ raise ValidationError("The type is certificate, you need to specify valid public and private key files")
991
+ super().save(*args, **kwargs)
992
+
993
+ class Meta:
994
+ verbose_name = "Import Credential"
995
+ verbose_name_plural = "Import Credentials"
996
+
997
+ def __str__(self) -> str:
998
+ dates_repr = ""
999
+ if self.validity_start:
1000
+ dates_repr += f"{self.validity_start:%Y-%m-%d %H:%M:%S}"
1001
+ if self.validity_end:
1002
+ dates_repr += f"- {self.validity_end:%Y-%m-%d %H:%M:%S}"
1003
+ if dates_repr:
1004
+ return f"{self.key} ({dates_repr})"
1005
+ return self.key