libreqda 1.2.0__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 (255) hide show
  1. CHANGELOG.md +481 -0
  2. libreqda-1.2.0.dist-info/LICENSE.txt +28 -0
  3. libreqda-1.2.0.dist-info/METADATA +131 -0
  4. libreqda-1.2.0.dist-info/RECORD +255 -0
  5. libreqda-1.2.0.dist-info/WHEEL +4 -0
  6. libreqda-1.2.0.dist-info/entry_points.txt +4 -0
  7. scripts/babelrc +8 -0
  8. scripts/dist/LICENSE.txt +33 -0
  9. scripts/dist/RELEASE.txt +23 -0
  10. scripts/dist/macos/libreqda.icns +0 -0
  11. scripts/dist/macos/macos.spec +43 -0
  12. scripts/dist/macos/plist.patch +10 -0
  13. scripts/dist/macos/taguette.icns +0 -0
  14. scripts/dist/macos/taguette_console_wrapper +4 -0
  15. scripts/dist/macos.sh +32 -0
  16. scripts/dist/pyinstaller_entrypoint.py +19 -0
  17. scripts/dist/windows/windows-installer.iss +51 -0
  18. scripts/dist/windows/windows.spec +39 -0
  19. scripts/edugain/worker_update_edugain_idps.py +89 -0
  20. scripts/edugain/worker_update_edugain_idps.sh +9 -0
  21. scripts/make_password_reset_link.py +56 -0
  22. scripts/make_theme.py +99 -0
  23. scripts/new_db_revision.sh +8 -0
  24. scripts/register_user.py +76 -0
  25. scripts/update_pot.sh +33 -0
  26. scripts/update_translations.sh +17 -0
  27. scripts/validate.py +140 -0
  28. scripts/wipe_and_disable_user.py +90 -0
  29. taguette/__init__.py +13 -0
  30. taguette/access_control.py +145 -0
  31. taguette/convert.py +623 -0
  32. taguette/database/__init__.py +177 -0
  33. taguette/database/base.py +9 -0
  34. taguette/database/copy.py +433 -0
  35. taguette/database/models.py +920 -0
  36. taguette/demodata.py +688 -0
  37. taguette/export.py +500 -0
  38. taguette/extract.py +287 -0
  39. taguette/import_codebook.py +110 -0
  40. taguette/l10n/de_DE/LC_MESSAGES/taguette_javascript.mo +0 -0
  41. taguette/l10n/de_DE/LC_MESSAGES/taguette_main.mo +0 -0
  42. taguette/l10n/el_EL/LC_MESSAGES/taguette_javascript.mo +0 -0
  43. taguette/l10n/el_GR/LC_MESSAGES/taguette_javascript.mo +0 -0
  44. taguette/l10n/el_GR/LC_MESSAGES/taguette_main.mo +0 -0
  45. taguette/l10n/en_US/LC_MESSAGES/taguette_javascript.mo +0 -0
  46. taguette/l10n/en_US/LC_MESSAGES/taguette_main.mo +0 -0
  47. taguette/l10n/es_ES/LC_MESSAGES/taguette_javascript.mo +0 -0
  48. taguette/l10n/es_ES/LC_MESSAGES/taguette_main.mo +0 -0
  49. taguette/l10n/fr_FR/LC_MESSAGES/taguette_javascript.mo +0 -0
  50. taguette/l10n/fr_FR/LC_MESSAGES/taguette_main.mo +0 -0
  51. taguette/l10n/id_ID/LC_MESSAGES/taguette_javascript.mo +0 -0
  52. taguette/l10n/id_ID/LC_MESSAGES/taguette_main.mo +0 -0
  53. taguette/l10n/it_IT/LC_MESSAGES/taguette_javascript.mo +0 -0
  54. taguette/l10n/it_IT/LC_MESSAGES/taguette_main.mo +0 -0
  55. taguette/l10n/nl_NL/LC_MESSAGES/taguette_javascript.mo +0 -0
  56. taguette/l10n/nl_NL/LC_MESSAGES/taguette_main.mo +0 -0
  57. taguette/l10n/pt_PT/LC_MESSAGES/taguette_javascript.mo +0 -0
  58. taguette/l10n/pt_PT/LC_MESSAGES/taguette_main.mo +0 -0
  59. taguette/l10n/sv_SE/LC_MESSAGES/taguette_javascript.mo +0 -0
  60. taguette/l10n/sv_SE/LC_MESSAGES/taguette_main.mo +0 -0
  61. taguette/loki_handler.py +107 -0
  62. taguette/main.py +649 -0
  63. taguette/migrations/env.py +72 -0
  64. taguette/migrations/script.py.mako +24 -0
  65. taguette/migrations/versions/09c662cd9483_make_documents_filename_not_null.py +39 -0
  66. taguette/migrations/versions/1b29e68900c1_add_user_roles.py +55 -0
  67. taguette/migrations/versions/1b96a606551d_update_user_to_have_a_display_name.py +34 -0
  68. taguette/migrations/versions/2467bab602b7_add_color_column_to_tags_table.py +33 -0
  69. taguette/migrations/versions/382db36f20f4_add_user_disabled.py +66 -0
  70. taguette/migrations/versions/43d6c240309d_remove_document_groups.py +81 -0
  71. taguette/migrations/versions/447d636f72c5_cascades.py +40 -0
  72. taguette/migrations/versions/491de2dc7cd7_add_indexes.py +51 -0
  73. taguette/migrations/versions/4c55b3b3ff30_user_login_length.py +58 -0
  74. taguette/migrations/versions/61a4abcc8769_user_with_parent_for_permissions.py +40 -0
  75. taguette/migrations/versions/6489b5f9cfb5_increase_password_hash_size.py +34 -0
  76. taguette/migrations/versions/679f625e6e6a_fix_unique_tags.py +59 -0
  77. taguette/migrations/versions/77c4543a5b9b_add_parent_id_column_to_collection_table.py +41 -0
  78. taguette/migrations/versions/7c94b7a31e69_add_is_archived_column_to_projets.py +39 -0
  79. taguette/migrations/versions/7f56de97e3c8_change_edugain_displayname_length.py +42 -0
  80. taguette/migrations/versions/807c0cc0ecf6_remove_invalid_hltags.py +43 -0
  81. taguette/migrations/versions/80b1cc9d4c22_add_user_email.py +35 -0
  82. taguette/migrations/versions/91ade71ccf4d_add_user_language.py +28 -0
  83. taguette/migrations/versions/955db2e523d1_add_collections_tables_and_associations_.py +60 -0
  84. taguette/migrations/versions/a1b2c3d4e5f6_add_user_hide_code_counts.py +28 -0
  85. taguette/migrations/versions/b23f3b7a638e_add_user_password_change_date.py +36 -0
  86. taguette/migrations/versions/b7c1e2d3f4a5_add_project_compact_layout.py +28 -0
  87. taguette/migrations/versions/bc8e0e0677e9_rename_command_fields.py +126 -0
  88. taguette/migrations/versions/bce44849c2f2_lowercase_logins.py +96 -0
  89. taguette/migrations/versions/ca076140922f_update_user_for_external_logins.py +32 -0
  90. taguette/migrations/versions/d86c45d7aa8a_add_document_direction.py +127 -0
  91. taguette/migrations/versions/db5e31a0233d_remove_blank_highlights.py +88 -0
  92. taguette/migrations/versions/dd37d9027a24_unique_tags.py +68 -0
  93. taguette/migrations/versions/de678cef2a94_add_project_id_to_collections_and_.py +41 -0
  94. taguette/migrations/versions/e459991d960e_add_table_edugainidps.py +39 -0
  95. taguette/migrations/versions/e4cf92942271_add_index.py +29 -0
  96. taguette/migrations/versions/e4e090a5b511_remove_commands_document_id_fk.py +38 -0
  97. taguette/migrations/versions/e9a9f1c77971_add_favorite_column_to_document.py +32 -0
  98. taguette/migrations/versions/ecb4065de575_don_t_use_server_default_now_timezone_.py +44 -0
  99. taguette/migrations/versions/fbdc29566f62_edugain_fix_field_type.py +42 -0
  100. taguette/migrations/versions/fdc6eb54b034_add_parent_id_column_to_tags_table.py +31 -0
  101. taguette/static/README.md +34 -0
  102. taguette/static/alt_templates/.gitkeep +0 -0
  103. taguette/static/assets/test/logos/monsters-university-logo.png +0 -0
  104. taguette/static/css/custom.css +1696 -0
  105. taguette/static/css/fa-all.min.css +5 -0
  106. taguette/static/css/themed.css +3 -0
  107. taguette/static/css/themed.css.map +25 -0
  108. taguette/static/css/vendor/bootstrap-treeview.min.css +1 -0
  109. taguette/static/css/vendor/bootstrap.min.css +6 -0
  110. taguette/static/css/vendor/font/context-menu-icons.eot +0 -0
  111. taguette/static/css/vendor/font/context-menu-icons.ttf +0 -0
  112. taguette/static/css/vendor/font/context-menu-icons.woff +0 -0
  113. taguette/static/css/vendor/font/context-menu-icons.woff2 +0 -0
  114. taguette/static/css/vendor/hierarchy-select.min.css +1 -0
  115. taguette/static/css/vendor/introjs.min.css +2 -0
  116. taguette/static/css/vendor/jquery.contextMenu.min.css +16 -0
  117. taguette/static/favicon.ico +0 -0
  118. taguette/static/js/codebook-viz.js +428 -0
  119. taguette/static/js/collection/context-menu.js +180 -0
  120. taguette/static/js/collection/hierarchy-select.js +71 -0
  121. taguette/static/js/collection/manager.js +217 -0
  122. taguette/static/js/document/context-menu.js +92 -0
  123. taguette/static/js/document/custom-select.js +82 -0
  124. taguette/static/js/document/manager.js +943 -0
  125. taguette/static/js/document/treeview.js +593 -0
  126. taguette/static/js/highlight/README.md +18 -0
  127. taguette/static/js/highlight/crud.js +119 -0
  128. taguette/static/js/highlight/extracts.js +497 -0
  129. taguette/static/js/highlight/loader.js +109 -0
  130. taguette/static/js/highlight/renderer.js +405 -0
  131. taguette/static/js/login.js +15 -0
  132. taguette/static/js/permissions.js +153 -0
  133. taguette/static/js/polling-event.js +145 -0
  134. taguette/static/js/project/intro.js +524 -0
  135. taguette/static/js/project/manager.js +0 -0
  136. taguette/static/js/project/search.js +124 -0
  137. taguette/static/js/tag/context-menu.js +133 -0
  138. taguette/static/js/tag/explore-codes.js +400 -0
  139. taguette/static/js/tag/hierarchy-select.js +69 -0
  140. taguette/static/js/tag/manager.js +545 -0
  141. taguette/static/js/tag/treeview.js +613 -0
  142. taguette/static/js/taguette.js +1790 -0
  143. taguette/static/js/utilities.js +714 -0
  144. taguette/static/js/vendor/bootstrap-treeview-custum.js +421 -0
  145. taguette/static/js/vendor/bootstrap-treeview-custum.min.js +1 -0
  146. taguette/static/js/vendor/bootstrap-treeview-default.min.js +1 -0
  147. taguette/static/js/vendor/bootstrap.bundle.min.js +7 -0
  148. taguette/static/js/vendor/bootstrap.bundle.min.js.map +1 -0
  149. taguette/static/js/vendor/hierarchy-select.min.js +1 -0
  150. taguette/static/js/vendor/introjs.min.js +17 -0
  151. taguette/static/js/vendor/jquery-3.7.1.js +2 -0
  152. taguette/static/js/vendor/jquery.contextMenu.min.js +2 -0
  153. taguette/static/libreqda.ico +0 -0
  154. taguette/static/logo-h.png +0 -0
  155. taguette/static/logo.png +0 -0
  156. taguette/static/logo_blanc.png +0 -0
  157. taguette/static/missing.png +0 -0
  158. taguette/static/robots.txt +2 -0
  159. taguette/static/scss/components/_badge.scss +42 -0
  160. taguette/static/scss/components/_button.scss +169 -0
  161. taguette/static/scss/components/_checkbox.scss +40 -0
  162. taguette/static/scss/components/_context_menu.scss +28 -0
  163. taguette/static/scss/components/_custom_select.scss +150 -0
  164. taguette/static/scss/components/_dropdown.scss +43 -0
  165. taguette/static/scss/components/_form.scss +54 -0
  166. taguette/static/scss/components/_modal.scss +57 -0
  167. taguette/static/scss/components/_project_card.scss +163 -0
  168. taguette/static/scss/components/_tag.scss +308 -0
  169. taguette/static/scss/components/_treeview.scss +76 -0
  170. taguette/static/scss/custom.scss +809 -0
  171. taguette/static/scss/layout/_credits.scss +62 -0
  172. taguette/static/scss/layout/_footer.scss +6 -0
  173. taguette/static/scss/layout/_navigation.scss +165 -0
  174. taguette/static/scss/layout/_sidebar.scss +37 -0
  175. taguette/static/scss/themes/default/_variables.scss +103 -0
  176. taguette/static/scss/themes/default/theme.css +3 -0
  177. taguette/static/scss/themes/default/theme.css.map +26 -0
  178. taguette/static/scss/themes/default/theme.scss +4 -0
  179. taguette/static/webfonts/BadScript-OFL.txt +94 -0
  180. taguette/static/webfonts/BadScript.ttf +0 -0
  181. taguette/static/webfonts/Montserrat-Light.ttf +0 -0
  182. taguette/static/webfonts/Montserrat-OFL.txt +93 -0
  183. taguette/static/webfonts/fa-brands-400.eot +0 -0
  184. taguette/static/webfonts/fa-brands-400.svg +1184 -0
  185. taguette/static/webfonts/fa-brands-400.ttf +0 -0
  186. taguette/static/webfonts/fa-brands-400.woff +0 -0
  187. taguette/static/webfonts/fa-brands-400.woff2 +0 -0
  188. taguette/static/webfonts/fa-regular-400.eot +0 -0
  189. taguette/static/webfonts/fa-regular-400.svg +467 -0
  190. taguette/static/webfonts/fa-regular-400.ttf +0 -0
  191. taguette/static/webfonts/fa-regular-400.woff +0 -0
  192. taguette/static/webfonts/fa-regular-400.woff2 +0 -0
  193. taguette/static/webfonts/fa-solid-900.eot +0 -0
  194. taguette/static/webfonts/fa-solid-900.svg +2618 -0
  195. taguette/static/webfonts/fa-solid-900.ttf +0 -0
  196. taguette/static/webfonts/fa-solid-900.woff +0 -0
  197. taguette/static/webfonts/fa-solid-900.woff2 +0 -0
  198. taguette/templates/alt_templates/.gitkeep +0 -0
  199. taguette/templates/archived_projects.html +50 -0
  200. taguette/templates/auth/account.html +74 -0
  201. taguette/templates/auth/cilogon.login.html +89 -0
  202. taguette/templates/auth/email_reset_password.html +5 -0
  203. taguette/templates/auth/external_account.html +60 -0
  204. taguette/templates/auth/login.html +144 -0
  205. taguette/templates/auth/reset_password.html +27 -0
  206. taguette/templates/auth/token_needed.html +7 -0
  207. taguette/templates/base.html +209 -0
  208. taguette/templates/bug_report.html +158 -0
  209. taguette/templates/cookies_prompt.html +15 -0
  210. taguette/templates/downloads.html +127 -0
  211. taguette/templates/email_bug_report.txt +9 -0
  212. taguette/templates/email_reset_password.txt +5 -0
  213. taguette/templates/error.html +8 -0
  214. taguette/templates/export_codebook.html +20 -0
  215. taguette/templates/export_document.html +20 -0
  216. taguette/templates/export_highlights.html +47 -0
  217. taguette/templates/index.html +347 -0
  218. taguette/templates/maintenance.html +8 -0
  219. taguette/templates/messages.js +74 -0
  220. taguette/templates/modal/change_document.html +68 -0
  221. taguette/templates/modal/create_collection.html +100 -0
  222. taguette/templates/modal/create_document.html +84 -0
  223. taguette/templates/modal/create_highlight.html +38 -0
  224. taguette/templates/modal/create_tag.html +73 -0
  225. taguette/templates/modal/explore_codes.html +58 -0
  226. taguette/templates/modal/manage_collaborators.html +44 -0
  227. taguette/templates/modal/merge_tag.html +49 -0
  228. taguette/templates/new_password.html +32 -0
  229. taguette/templates/partials/alert.html +16 -0
  230. taguette/templates/partials/codebook_viz_modal.html +34 -0
  231. taguette/templates/partials/confirm.html +17 -0
  232. taguette/templates/partials/document_toolbar.html +37 -0
  233. taguette/templates/partials/hlinfo.html +13 -0
  234. taguette/templates/partials/project_card.html +44 -0
  235. taguette/templates/partials/project_card_archived.html +41 -0
  236. taguette/templates/partials/project_sidebar.html +99 -0
  237. taguette/templates/partials/spinner.html +11 -0
  238. taguette/templates/permissions.html +54 -0
  239. taguette/templates/project.html +288 -0
  240. taguette/templates/project_delete.html +17 -0
  241. taguette/templates/project_import.html +122 -0
  242. taguette/templates/project_import_codebook.html +34 -0
  243. taguette/templates/project_import_codebook_confirm.html +49 -0
  244. taguette/templates/project_new.html +31 -0
  245. taguette/templates/tos.html +8 -0
  246. taguette/templates/trans.js +7 -0
  247. taguette/templates/welcome.html +15 -0
  248. taguette/utils.py +177 -0
  249. taguette/validate.py +141 -0
  250. taguette/version_check.py +102 -0
  251. taguette/web/__init__.py +252 -0
  252. taguette/web/api.py +1835 -0
  253. taguette/web/base.py +864 -0
  254. taguette/web/export.py +338 -0
  255. taguette/web/views.py +1618 -0
@@ -0,0 +1,714 @@
1
+ /*
2
+ * Utilities
3
+ */
4
+
5
+ // NEW selector
6
+ jQuery.expr[':'].contains_insensitive = function(a, i, m) {
7
+ return jQuery(a).text().toUpperCase()
8
+ .indexOf(m[3].toUpperCase()) >= 0;
9
+ };
10
+
11
+ if (!Object.entries) {
12
+ Object.entries = function (obj) {
13
+ let ownProps = Object.keys(obj),
14
+ i = ownProps.length,
15
+ resArray = new Array(i); // preallocate the Array
16
+ while (i--) {
17
+ resArray[i] = [ownProps[i], obj[ownProps[i]]];
18
+ }
19
+
20
+ return resArray;
21
+ };
22
+ }
23
+
24
+ function sortByKey(array, key, reverse) {
25
+ let orderByX = 1;
26
+ if (reverse) {
27
+ orderByX = -1
28
+ }
29
+ array.sort(function (a, b) {
30
+ if (key(a) < key(b)) {
31
+ return -1 * orderByX;
32
+ } else if (key(a) > key(b)) {
33
+ return orderByX;
34
+ } else {
35
+ return 0;
36
+ }
37
+ });
38
+ }
39
+
40
+ function encodeGetParams(params) {
41
+ return Object.entries(params)
42
+ .filter(function (kv) {
43
+ return kv[1] !== undefined;
44
+ })
45
+ .map(function (kv) {
46
+ return kv.map(encodeURIComponent).join("=");
47
+ })
48
+ .join("&");
49
+ }
50
+
51
+ // Don't use RegExp literals https://github.com/python-babel/babel/issues/640
52
+ let _escapeA = new RegExp('&', 'g'),
53
+ _escapeL = new RegExp('<', 'g'),
54
+ _escapeG = new RegExp('>', 'g'),
55
+ _escapeQ = new RegExp('"', 'g'),
56
+ _escapeP = new RegExp("'", 'g');
57
+
58
+ function escapeHtml(s) {
59
+ return s
60
+ .replace(_escapeA, "&amp;")
61
+ .replace(_escapeL, "&lt;")
62
+ .replace(_escapeG, "&gt;")
63
+ .replace(_escapeQ, "&quot;")
64
+ .replace(_escapeP, "&#039;");
65
+ }
66
+
67
+ function nextElement(node) {
68
+ while (node && !node.nextSibling) {
69
+ node = node.parentNode;
70
+ }
71
+ if (!node) {
72
+ return null;
73
+ }
74
+ node = node.nextSibling;
75
+ while (node.firstChild) {
76
+ node = node.firstChild;
77
+ }
78
+ return node;
79
+ }
80
+
81
+ let onlyWhitespace = new RegExp('^[\r\n\t]*$');
82
+
83
+ function getScrollPos() {
84
+ var doc = document.scrollingElement || document.documentElement, body = document.body;
85
+ var x = (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc.clientLeft || 0);
86
+ var y = (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc.clientTop || 0);
87
+ return {x: x, y: y};
88
+ }
89
+
90
+ // Lets users start typing anywhere inside a container (e.g. a modal or panel)
91
+ // to jump into its search input, instead of requiring a click into the box first.
92
+ function enableTypeToFocusSearch(containerSelector, inputSelector) {
93
+ const container = document.querySelector(containerSelector);
94
+ const input = document.querySelector(inputSelector);
95
+ if (!container || !input) return;
96
+
97
+ const interactiveTags = ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON', 'A'];
98
+
99
+ container.addEventListener('keydown', function (event) {
100
+ if (document.activeElement === input) return;
101
+ if (event.ctrlKey || event.metaKey || event.altKey) return;
102
+ if (event.key.length !== 1) return; // ignore Tab/Escape/Enter/arrows/etc., only printable chars
103
+
104
+ const target = event.target;
105
+ if (interactiveTags.includes(target.tagName) || target.isContentEditable) return;
106
+
107
+ input.focus();
108
+ });
109
+ }
110
+
111
+ function getCookie(name) {
112
+ const r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
113
+ return r ? r[1] : undefined;
114
+ }
115
+
116
+ function ApiError(response, message) {
117
+ if (!message) {
118
+ message = "Status " + response.status;
119
+ }
120
+ this.status = response.status;
121
+ this.message = message;
122
+ }
123
+
124
+ ApiError.prototype.toString = function () {
125
+ return this.message;
126
+ };
127
+
128
+ function getJSON(url = '', args) {
129
+ if (args) {
130
+ args = '?' + encodeGetParams(args);
131
+ } else {
132
+ args = '';
133
+ }
134
+ return fetch(
135
+ base_path + url + args,
136
+ {
137
+ credentials: 'same-origin',
138
+ mode: 'same-origin',
139
+ redirect: 'error'
140
+ }
141
+ ).then(function (response) {
142
+ if (response.status !== 200) {
143
+ return response.json()
144
+ .then(
145
+ function (json) {
146
+ if ("error" in json) {
147
+ throw new ApiError(response, json.error);
148
+ } else {
149
+ throw new ApiError(response);
150
+ }
151
+ },
152
+ function () {
153
+ throw new ApiError(response);
154
+ }
155
+ );
156
+ }
157
+ return response.json().catch(function () {
158
+ throw new ApiError(response, "Invalid JSON");
159
+ });
160
+ });
161
+ }
162
+
163
+ function deleteURL(url = '', args) {
164
+ if (args) {
165
+ args = encodeGetParams(args);
166
+ } else {
167
+ args = '';
168
+ }
169
+ let xsrf = getCookie('_xsrf');
170
+ return fetch(
171
+ base_path + url + '?' + (xsrf !== undefined ? '_xsrf=' + encodeURIComponent(xsrf) + '&' : '') + args,
172
+ {
173
+ credentials: 'same-origin',
174
+ mode: 'same-origin',
175
+ cache: 'no-cache',
176
+ redirect: 'error',
177
+ method: 'DELETE'
178
+ }
179
+ ).then(function (response) {
180
+ if (response.status !== 204) {
181
+ throw new ApiError(response);
182
+ }
183
+ });
184
+ }
185
+
186
+ function postJSON(url = '', data = {}, args) {
187
+ if (args) {
188
+ args = encodeGetParams(args);
189
+ } else {
190
+ args = '';
191
+ }
192
+ let xsrf = getCookie('_xsrf');
193
+ return fetch(
194
+ base_path + url + '?' + (xsrf !== undefined ? '_xsrf=' + encodeURIComponent(xsrf) + '&' : '') + args,
195
+ {
196
+ credentials: 'same-origin',
197
+ mode: 'same-origin',
198
+ cache: 'no-cache',
199
+ redirect: 'error',
200
+ method: 'POST',
201
+ headers: {
202
+ 'Content-Type': 'application/json; charset=utf-8'
203
+ },
204
+ body: JSON.stringify(data)
205
+ }
206
+ ).then(function (response) {
207
+ if (response.status !== 200) {
208
+ return response.json()
209
+ .then(
210
+ function (json) {
211
+ if ("error" in json) {
212
+ throw new ApiError(response, json.error);
213
+ } else {
214
+ throw new ApiError(response);
215
+ }
216
+ },
217
+ function () {
218
+ throw new ApiError(response);
219
+ }
220
+ );
221
+ }
222
+ return response.json().catch(function () {
223
+ throw new ApiError(response, "Invalid JSON");
224
+ });
225
+ });
226
+ }
227
+
228
+ function patchJSON(url = '', data = {}, args) {
229
+ if (args) {
230
+ args = encodeGetParams(args);
231
+ } else {
232
+ args = '';
233
+ }
234
+ let xsrf = getCookie('_xsrf');
235
+ return fetch(
236
+ base_path + url + '?' + (xsrf !== undefined ? '_xsrf=' + encodeURIComponent(xsrf) + '&' : '') + args,
237
+ {
238
+ credentials: 'same-origin',
239
+ mode: 'same-origin',
240
+ cache: 'no-cache',
241
+ redirect: 'error',
242
+ method: 'PATCH',
243
+ headers: {
244
+ 'Content-Type': 'application/json; charset=utf-8'
245
+ },
246
+ body: JSON.stringify(data)
247
+ }
248
+ ).then(function (response) {
249
+ if (response.status !== 204) {
250
+ return response.json()
251
+ .then(
252
+ function (json) {
253
+ if ("error" in json) {
254
+ throw new ApiError(response, json.error);
255
+ } else {
256
+ throw new ApiError(response);
257
+ }
258
+ },
259
+ function () {
260
+ throw new ApiError(response);
261
+ }
262
+ );
263
+ } else {
264
+ return null;
265
+ }
266
+ });
267
+ }
268
+
269
+ // Build a lookup table mapping UTF-8 byte offset -> char index for a string.
270
+ function buildUTF8ToCharMap(str) {
271
+ const map = [];
272
+ let byteOffset = 0;
273
+ for (let i = 0; i < str.length; i++) {
274
+ const code = str.codePointAt(i);
275
+ let byteLen;
276
+ if (code <= 0x7f) byteLen = 1;
277
+ else if (code <= 0x7ff) byteLen = 2;
278
+ else if (code <= 0xffff) byteLen = 3;
279
+ else { byteLen = 4; i++; } // skip surrogate pair
280
+ for (let b = 0; b < byteLen; b++) {
281
+ map[byteOffset + b] = i;
282
+ }
283
+ byteOffset += byteLen;
284
+ }
285
+ map[byteOffset] = str.length; // sentinel for end offset
286
+ return map;
287
+ }
288
+
289
+ // Returns the byte length of a string encoded in UTF-8
290
+ // https://stackoverflow.com/q/5515869/711380
291
+ function getCharIndexFromUTF8Index(str, utf8Index, utf8ToCharMap) {
292
+ if (utf8ToCharMap) {
293
+ return utf8ToCharMap[utf8Index] !== undefined ? utf8ToCharMap[utf8Index] : str.length;
294
+ }
295
+ let charIndex = 0;
296
+ let currentUTF8Index = 0;
297
+ while (currentUTF8Index < utf8Index && charIndex < str.length) {
298
+ currentUTF8Index += lengthUTF8(str.charAt(charIndex));
299
+ charIndex++;
300
+ }
301
+ return charIndex;
302
+ }
303
+
304
+ const _utf8Encoder = typeof TextEncoder !== 'undefined' ? new TextEncoder() : null;
305
+
306
+ function lengthUTF8(s) {
307
+ if (_utf8Encoder) {
308
+ return _utf8Encoder.encode(s).length;
309
+ } else {
310
+ let l = s.length;
311
+ for (let i = s.length - 1; i >= 0; --i) {
312
+ let code = s.charCodeAt(i);
313
+ if (code > 0x7f && code <= 0x7ff) ++l;
314
+ else if (code > 0x7ff && code <= 0xffff) l += 2;
315
+ if (code >= 0xDC00 && code <= 0xDFFF) i--; // trailing surrogate
316
+ }
317
+ return l;
318
+ }
319
+ }
320
+
321
+ // Escape a string for use in a regexp
322
+ // https://stackoverflow.com/a/6969486/711380
323
+ function escapeRegExp(string) {
324
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
325
+ }
326
+
327
+ window.addEventListener('load', function () {
328
+ // https://css-tricks.com/snippets/jquery/draggable-without-jquery-ui/
329
+ (function ($) {
330
+ $.fn.drags = function (opt) {
331
+ opt = $.extend({handle: "", cursor: "move"}, opt);
332
+ let $el;
333
+ let $drag;
334
+ if (opt.handle === "") {
335
+ $el = this;
336
+ } else {
337
+ $el = this.find(opt.handle);
338
+ }
339
+
340
+ return $el.css('cursor', opt.cursor).on("mousedown", function (e) {
341
+ if (opt.handle === "") {
342
+ $drag = $(this).addClass('draggable');
343
+ } else {
344
+ $drag = $(this).addClass('active-handle').parent().addClass('draggable');
345
+ }
346
+ let z_idx = $drag.css('z-index'),
347
+ drg_h = $drag.outerHeight(),
348
+ drg_w = $drag.outerWidth(),
349
+ pos_y = $drag.offset().top + drg_h - e.pageY,
350
+ pos_x = $drag.offset().left + drg_w - e.pageX;
351
+ $drag.css('z-index', 1000).parents().on("mousemove", function (e) {
352
+ $('.draggable').offset({
353
+ top: e.pageY + pos_y - drg_h,
354
+ left: e.pageX + pos_x - drg_w
355
+ }).on("mouseup", function () {
356
+ $(this).removeClass('draggable').css('z-index', z_idx);
357
+ });
358
+ });
359
+ e.preventDefault(); // disable selection
360
+ }).on("mouseup", function () {
361
+ if (opt.handle === "") {
362
+ $(this).removeClass('draggable');
363
+ } else {
364
+ $(this).removeClass('active-handle').parent().removeClass('draggable');
365
+ }
366
+ });
367
+ }
368
+ })(jQuery);
369
+ });
370
+
371
+ function showAlert(message, status) {
372
+
373
+ const alertModal = $('#alertModal');
374
+
375
+ let modalTitle = '';
376
+ let alertClass = '';
377
+ let modalContent = '';
378
+ let iconClass = '';
379
+
380
+ switch (status) {
381
+ case 'info':
382
+ modalTitle = 'Information';
383
+ alertClass = 'alert-info';
384
+ modalContent = 'This is some information content.';
385
+ iconClass = 'fa-info-circle';
386
+ break;
387
+ case 'error':
388
+ modalTitle = 'Erreur';
389
+ alertClass = 'alert-danger';
390
+ modalContent = 'This is some error content.';
391
+ iconClass = 'fa-exclamation-triangle';
392
+ break;
393
+ case 'warning':
394
+ modalTitle = 'Avertissement';
395
+ alertClass = 'alert-warning';
396
+ modalContent = 'This is some warning content.';
397
+ iconClass = 'fa-exclamation-circle';
398
+ break;
399
+ default:
400
+ modalTitle = 'Alerte';
401
+ alertClass = 'alert-primary';
402
+ modalContent = 'This is some default content.';
403
+ iconClass = 'fa-info';
404
+ }
405
+
406
+ $('#alertModalLabel').text(modalTitle);
407
+
408
+ const alertMessageHtml = `
409
+ <div class="alert ${alertClass}" role="alert">
410
+ <i class="fa ${iconClass}"></i> <strong>${message}</strong>
411
+ </div>
412
+ `;
413
+
414
+ $('#alertMessage').html(alertMessageHtml)
415
+ alertModal.modal('show');
416
+
417
+ alertModal.on('hidden.bs.modal', function () {
418
+ $('#alertMessage').html('');
419
+ });
420
+ }
421
+
422
+ function showConfirm(message, confirmCallback, show_danger_button=false) {
423
+
424
+ $('#confirmMessage').html(message);
425
+
426
+ if(show_danger_button) {
427
+ $('#confirmButton').addClass('btn-danger');
428
+ $('#confirmButton').removeClass('btn-primary');
429
+ } else {
430
+ $('#confirmButton').addClass('btn-primary');
431
+ $('#confirmButton').removeClass('btn-danger');
432
+ }
433
+
434
+ $('#confirmModal').modal('show');
435
+
436
+ $('#confirmButton').off('click').on('click', function () {
437
+ if (typeof confirmCallback === 'function') {
438
+ confirmCallback();
439
+ }
440
+
441
+ $('#confirmModal').modal('hide');
442
+ });
443
+ }
444
+
445
+ function log(message, status = 'info', context = {}) {
446
+ const timestamp = new Date().toLocaleString();
447
+ const logMessage = `${timestamp} [${status.toUpperCase()}]: ${message}`;
448
+ if (status.toLowerCase() === 'error') {
449
+ // console.error(logMessage, context);
450
+ } else {
451
+ console.log(logMessage, context);
452
+ }
453
+ }
454
+
455
+ function showSpinner(cancel_action=null, show_progress=false) {
456
+ let $spinner_modal = $('#spinner-modal')
457
+ if (typeof $spinner_modal.modal !== 'undefined') {
458
+ $spinner_modal.modal('show');
459
+ if(cancel_action !== null) {
460
+ $('button.cancel', $spinner_modal).show();
461
+ $('button.cancel', $spinner_modal).bind("click", cancel_action);
462
+ }
463
+ if(show_progress) {
464
+ $("#progress", $spinner_modal).show();
465
+ } else {
466
+ $("#progress", $spinner_modal).hide();
467
+ }
468
+ }
469
+ }
470
+
471
+ function updateProgress(percent) {
472
+ function doUpdateProgress(){
473
+ let $spinner_modal = $('#spinner-modal');
474
+ if (typeof $spinner_modal.modal !== 'undefined') {
475
+ if (isNaN(percent) || (percent < 0)) {
476
+ percent = 0;
477
+ }
478
+ if (percent > 100) {
479
+ percent = 100;
480
+ }
481
+ $("#progress .progress-bar", $spinner_modal).css('width', percent + '%');
482
+ $("#progress .progress-bar", $spinner_modal).css('aria-valuenow', percent);
483
+ log(`Upload progress: ${percent}%`);
484
+ }
485
+ }
486
+ window.requestAnimationFrame(doUpdateProgress);
487
+ }
488
+
489
+ function hideSpinner() {
490
+ let $spinner_modal = $('#spinner-modal');
491
+ $spinner_modal.modal('hide');
492
+ $('button.cancel', $spinner_modal).hide();
493
+ $('button.cancel', $spinner_modal).unbind("click");
494
+ }
495
+
496
+
497
+ /*
498
+ * Selection stuff
499
+ */
500
+
501
+ let chunk_offsets = [];
502
+
503
+ // Get the document offset from a position
504
+ function describePos(node, offset) {
505
+ // Convert current offset from character to bytes
506
+ offset = lengthUTF8(node.textContent.substring(0, offset));
507
+ while (!node.id) {
508
+ if (node.previousSibling) {
509
+ node = node.previousSibling;
510
+ offset += lengthUTF8(node.textContent);
511
+ } else {
512
+ node = node.parentNode;
513
+ }
514
+ }
515
+ if (node.id.substring(0, 11) !== 'doc-offset-') {
516
+ return null;
517
+ }
518
+ return parseInt(node.id.substring(11)) + offset;
519
+ }
520
+
521
+ // Find a position from the document offset
522
+ function locatePos(pos) {
523
+ // Find the right chunk
524
+ let chunk_start = 0;
525
+ for (let i = 0; i < chunk_offsets.length; ++i) {
526
+ if (chunk_offsets[i] > pos) {
527
+ break;
528
+ }
529
+ chunk_start = chunk_offsets[i];
530
+ }
531
+
532
+ let offset = pos - chunk_start;
533
+ let node = document.getElementById('doc-offset-' + chunk_start);
534
+ while (node.firstChild) {
535
+ node = node.firstChild;
536
+ }
537
+ while (offset > 0) {
538
+ if (lengthUTF8(node.textContent) >= offset) {
539
+ break;
540
+ } else {
541
+ offset -= lengthUTF8(node.textContent);
542
+ node = nextElement(node);
543
+ }
544
+ }
545
+ return [node, offset]
546
+ }
547
+
548
+ let current_selection = null;
549
+ let current_selection_text = null;
550
+
551
+ function describeSelection() {
552
+ const sel = window.getSelection();
553
+ if (sel.rangeCount === 0 || sel.isCollapsed) return null;
554
+
555
+ const range = sel.getRangeAt(0);
556
+ const container = document.getElementById('highlight-content');
557
+ if (!container || !container.contains(range.commonAncestorContainer)) return null;
558
+
559
+ const tempRange = document.createRange();
560
+ tempRange.selectNodeContents(container);
561
+ tempRange.setEnd(range.startContainer, range.startOffset);
562
+ const startOffset = lengthUTF8(tempRange.toString());
563
+
564
+ tempRange.selectNodeContents(container);
565
+ tempRange.setEnd(range.endContainer, range.endOffset);
566
+ const endOffset = lengthUTF8(tempRange.toString());
567
+
568
+ return [startOffset, endOffset];
569
+ }
570
+
571
+ function splitAtPos(pos, after) {
572
+ let node = pos[0], idx = pos[1];
573
+ if (idx === 0) {
574
+ // Leftmost index: return current node
575
+ return node;
576
+ } else if (idx >= lengthUTF8(node.textContent)) {
577
+ // Rightmost index: return next node
578
+ return nextElement(node);
579
+ } else {
580
+ // Find the character index from the byte index
581
+ let idx_char = 0;
582
+ while (idx > 0) {
583
+ let code = node.textContent.charCodeAt(idx_char);
584
+ if (code <= 0x7f) idx -= 1;
585
+ else if (code <= 0x7ff) idx -= 2;
586
+ else if (code <= 0xffff) idx -= 3;
587
+ else idx -= 4;
588
+ idx_char += 1;
589
+ }
590
+ if (idx_char >= node.textContent.length) {
591
+ log("Error computing character position!", 'error');
592
+ idx_char = node.textContent.length - 1;
593
+ }
594
+ // Split, return right node
595
+ return node.splitText(idx_char);
596
+ }
597
+ }
598
+
599
+ // Restore a described selection
600
+ function restoreSelection(saved) {
601
+ let sel = window.getSelection();
602
+ sel.removeAllRanges();
603
+ if (saved !== null) {
604
+ let range = document.createRange();
605
+ let start = locatePos(saved[0]);
606
+ let end = locatePos(saved[1]);
607
+ range.setStart(start[0], start[1]);
608
+ range.setEnd(end[0], end[1]);
609
+ sel.addRange(range);
610
+ }
611
+ }
612
+
613
+ function isFirefox() {
614
+ return navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
615
+ }
616
+
617
+ function hasVerticalScrollbar() {
618
+ // Get the total height of the document content
619
+ const documentHeight = Math.max(
620
+ document.body.scrollHeight,
621
+ document.documentElement.scrollHeight,
622
+ document.body.offsetHeight,
623
+ document.documentElement.offsetHeight,
624
+ document.body.clientHeight,
625
+ document.documentElement.clientHeight
626
+ );
627
+
628
+ // Get the height of the viewport (visible area)
629
+ const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
630
+
631
+ // Compare the document height with the viewport height
632
+ return documentHeight > viewportHeight;
633
+ }
634
+
635
+ /*
636
+ * Apply scroll behavior to all pages
637
+ * Previously page-project only (taguette.js)
638
+ */
639
+
640
+ window.addEventListener('load', function() {
641
+ let lastScrollY = 0;
642
+ let scrollUpDistance = 0;
643
+ const bodyElement = document.querySelector('body');
644
+ if (!bodyElement) return;
645
+
646
+ window.addEventListener('scroll', function() {
647
+ const currentY = window.scrollY;
648
+
649
+ if (currentY < 50) {
650
+ bodyElement.classList.remove("scrolled");
651
+ scrollUpDistance = 0;
652
+ } else if (currentY > lastScrollY) {
653
+ // Scrolling down — hide navbar
654
+ scrollUpDistance = 0;
655
+ bodyElement.classList.add("scrolled");
656
+ } else {
657
+ // Scrolling up — show after 80px
658
+ scrollUpDistance += lastScrollY - currentY;
659
+ if (scrollUpDistance > 80) {
660
+ bodyElement.classList.remove("scrolled");
661
+ }
662
+ }
663
+ lastScrollY = currentY;
664
+ });
665
+ });
666
+
667
+ function initHelpPopover(elementId, sections) {
668
+ const contentParts = sections.map((section, index) => {
669
+ const itemsList = section.items.map(item => `<li>${gettext(item)}</li>`).join('');
670
+ const marginClass = index === sections.length - 1 ? 'mb-0' : 'mb-2';
671
+ return `
672
+ <p class="mb-2"><strong>${gettext(section.title)}</strong></p>
673
+ <ul class="${marginClass} ps-3">
674
+ ${itemsList}
675
+ </ul>
676
+ `;
677
+ }).join('');
678
+
679
+ const helpContent = `<div class="text-start">${contentParts}</div>`;
680
+
681
+ const popoverElement = document.getElementById(elementId);
682
+ if (popoverElement) {
683
+ const existing = bootstrap.Popover.getInstance(popoverElement);
684
+ if (existing) existing.dispose();
685
+
686
+ const popover = new bootstrap.Popover(popoverElement, {
687
+ content: helpContent,
688
+ html: true,
689
+ sanitize: false,
690
+ trigger: 'manual',
691
+ placement: 'left',
692
+ fallbackPlacements: ['top', 'bottom', 'right']
693
+ });
694
+
695
+ if (popoverElement._helpClickHandler) {
696
+ popoverElement.removeEventListener('click', popoverElement._helpClickHandler);
697
+ }
698
+ popoverElement._helpClickHandler = (e) => {
699
+ e.stopPropagation();
700
+ popover.toggle();
701
+ };
702
+ popoverElement.addEventListener('click', popoverElement._helpClickHandler);
703
+
704
+ if (!popoverElement._helpOutsideHandler) {
705
+ popoverElement._helpOutsideHandler = (e) => {
706
+ const tip = document.querySelector('.popover');
707
+ if (tip && !tip.contains(e.target) && e.target !== popoverElement) {
708
+ popover.hide();
709
+ }
710
+ };
711
+ document.addEventListener('click', popoverElement._helpOutsideHandler);
712
+ }
713
+ }
714
+ }