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,109 @@
1
+ let highlightResizeObserver = null;
2
+
3
+ function loadDocumentHighlights(document_id, highlight_id, splitParagraphs, search_text) {
4
+ if (document_id === null) {
5
+ document_contents.style.direction = 'ltr';
6
+ document_contents.innerHTML = '<p style="font-style: oblique; text-align: center;">' +
7
+ gettext("Load a document on the left") + '</p>';
8
+ toggleToolbarActions(false);
9
+ return;
10
+ }
11
+
12
+ showSpinner();
13
+
14
+ const info = getJSON(`/api/project/${project_id}/document/${document_id}`);
15
+ const contents = getJSON(`/api/project/${project_id}/document/${document_id}/contents`);
16
+
17
+ Promise.all([info, contents])
18
+ .then(function(results) {
19
+ const info = results[0];
20
+ const contents = results[1];
21
+
22
+ document_contents.innerHTML = '';
23
+ highlights = {};
24
+
25
+ if (highlightResizeObserver) {
26
+ highlightResizeObserver.disconnect();
27
+ highlightResizeObserver = null;
28
+ }
29
+
30
+ const mainContainer = document.createElement('div');
31
+ mainContainer.style.position = 'relative';
32
+ mainContainer.style.width = '100%';
33
+
34
+ const contentDiv = document.createElement('div');
35
+ contentDiv.id = 'highlight-content';
36
+ contentDiv.style.userSelect = 'text';
37
+ contentDiv.style.position = 'relative';
38
+ contentDiv.style.direction = info.text_direction === 'RIGHT_TO_LEFT' ? 'rtl' : 'ltr';
39
+ contentDiv.style.padding = '20px';
40
+ contentDiv.style.lineHeight = '1.6';
41
+
42
+ const allHTML = contents.contents.map((chunk, index) => {
43
+ return `<div id="doc-offset-${chunk.offset}" class="document-chunk" data-chunk-index="${index}">${chunk.contents}</div>`;
44
+ }).join('\n');
45
+
46
+ contentDiv.innerHTML = allHTML;
47
+ disableEditing(contentDiv);
48
+ addDragFunctionality(contentDiv);
49
+ mainContainer.appendChild(contentDiv);
50
+
51
+ // SVG overlay drawn on top of text — pointer-events: none so it doesn't block selection
52
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
53
+ svg.id = 'highlights-overlay';
54
+ svg.style.position = 'absolute';
55
+ svg.style.top = '0';
56
+ svg.style.left = '0';
57
+ svg.style.width = '100%';
58
+ svg.style.height = '100%';
59
+ svg.style.pointerEvents = 'none';
60
+ svg.style.zIndex = '9';
61
+ svg.style.overflow = 'visible';
62
+
63
+ mainContainer.appendChild(svg);
64
+ document_contents.appendChild(mainContainer);
65
+
66
+ for (const element of info.highlights) {
67
+ const id = '' + element.id;
68
+ highlights[id] = element;
69
+ }
70
+
71
+ requestAnimationFrame(() => {
72
+ requestAnimationFrame(() => {
73
+ try {
74
+ renderHighlights(Object.values(highlights));
75
+ } catch (error) {
76
+ log("Error rendering highlights", 'error', error);
77
+ }
78
+ });
79
+ });
80
+
81
+ let _resizeTimeout = null;
82
+ highlightResizeObserver = new ResizeObserver(() => {
83
+ if (_renderingInProgress) return;
84
+ if (_resizeTimeout) clearTimeout(_resizeTimeout);
85
+ _resizeTimeout = setTimeout(() => {
86
+ try {
87
+ renderHighlights(Object.values(highlights));
88
+ } catch (error) {
89
+ log("Error re-rendering on resize", 'error', error);
90
+ }
91
+ }, 300);
92
+ });
93
+ highlightResizeObserver.observe(contentDiv);
94
+
95
+ current_document = document_id;
96
+ toggleToolbarActions(true);
97
+ updateDocumentLinkHighlight(current_document);
98
+ updateDocumentExportButton(project_id, document_id);
99
+ updateLinksTarget(document_contents);
100
+
101
+ setTimeout(() => scrollToHighlight(highlight_id, search_text), 200);
102
+ })
103
+ .catch(function(error) {
104
+ log("Failed to load document", 'error', error);
105
+ const loadDocumentErrorMessage = gettext("Error loading document!");
106
+ showAlert(`${loadDocumentErrorMessage} \n\n`, 'error');
107
+ })
108
+ .then(hideSpinner);
109
+ }
@@ -0,0 +1,405 @@
1
+ const LINE_HEIGHT = 3;
2
+ const LINE_GAP = 2;
3
+ const BASE_OFFSET = 2;
4
+
5
+ let _renderingInProgress = false;
6
+
7
+ function renderHighlights(hlList) {
8
+ const container = document.getElementById('highlight-content');
9
+ const svg = document.getElementById('highlights-overlay');
10
+ if (!svg || !container) return;
11
+
12
+ _renderingInProgress = true;
13
+
14
+ const targets = getBlockTargets(container);
15
+ targets.forEach(el => { el.style.lineHeight = ''; });
16
+
17
+ // Pre-compute shared data once
18
+ const textNodes = collectTextNodes(container);
19
+ const { fullText, charMap } = buildCharMap(textNodes);
20
+ const utf8Map = buildUTF8ToCharMap(fullText);
21
+
22
+ const targetMaxLanes = estimateMaxLanesPerTarget(hlList, targets, container, textNodes, fullText, utf8Map);
23
+
24
+ let layoutChanged = false;
25
+ targetMaxLanes.forEach((maxLanes, el) => {
26
+ const newLH = (1.6 + maxLanes * 0.18) + 'em';
27
+ if (el.style.lineHeight !== newLH) {
28
+ el.style.lineHeight = newLH;
29
+ layoutChanged = true;
30
+ }
31
+ });
32
+
33
+ if (layoutChanged) {
34
+ requestAnimationFrame(() => {
35
+ requestAnimationFrame(() => {
36
+ drawHighlights(hlList, container, svg, charMap, fullText, utf8Map);
37
+ _renderingInProgress = false;
38
+ });
39
+ });
40
+ } else {
41
+ drawHighlights(hlList, container, svg, charMap, fullText, utf8Map);
42
+ _renderingInProgress = false;
43
+ }
44
+ }
45
+
46
+ function estimateMaxLanesPerTarget(hlList, targets, container, textNodes, fullText, utf8Map) {
47
+ if (hlList.length === 0) return new Map();
48
+
49
+ const nodeToTarget = new Map();
50
+ textNodes.forEach(node => {
51
+ let el = node.parentElement;
52
+ while (el && el.id !== 'highlight-content') {
53
+ if (targets.includes(el)) {
54
+ nodeToTarget.set(node, el);
55
+ break;
56
+ }
57
+ el = el.parentElement;
58
+ }
59
+ });
60
+
61
+ const charToTarget = [];
62
+ textNodes.forEach(node => {
63
+ const target = nodeToTarget.get(node);
64
+ for (const _ of node.textContent) {
65
+ charToTarget.push(target);
66
+ }
67
+ });
68
+
69
+ const targetHighlights = new Map();
70
+ hlList.forEach(hl => {
71
+ const startChar = getCharIndexFromUTF8Index(fullText, hl.start_offset, utf8Map);
72
+ const endChar = getCharIndexFromUTF8Index(fullText, hl.end_offset, utf8Map);
73
+ const midChar = Math.max(0, Math.min(charToTarget.length - 1, Math.floor((startChar + endChar) / 2)));
74
+ const target = charToTarget[midChar];
75
+ if (target) {
76
+ if (!targetHighlights.has(target)) targetHighlights.set(target, []);
77
+ targetHighlights.get(target).push(hl);
78
+ }
79
+ });
80
+
81
+ const result = new Map();
82
+ targetHighlights.forEach((hls, target) => {
83
+ const sorted = hls.slice().sort((a, b) => a.start_offset - b.start_offset);
84
+ const laneEnds = [];
85
+ for (const hl of sorted) {
86
+ let placed = false;
87
+ for (let i = 0; i < laneEnds.length; i++) {
88
+ if (laneEnds[i] <= hl.start_offset) {
89
+ laneEnds[i] = hl.end_offset;
90
+ placed = true;
91
+ break;
92
+ }
93
+ }
94
+ if (!placed) laneEnds.push(hl.end_offset);
95
+ }
96
+ result.set(target, laneEnds.length);
97
+ });
98
+
99
+ return result;
100
+ }
101
+
102
+ function getBlockTargets(container) {
103
+ const targets = [];
104
+ const chunksWithBlocks = new Set();
105
+ const blockSelectors = '.document-chunk p, .document-chunk h1, .document-chunk h2, .document-chunk h3, .document-chunk h4, .document-chunk h5, .document-chunk h6, .document-chunk li, .document-chunk blockquote';
106
+
107
+ container.querySelectorAll(blockSelectors).forEach(el => {
108
+ targets.push(el);
109
+ const chunk = el.closest('.document-chunk');
110
+ if (chunk) chunksWithBlocks.add(chunk);
111
+ });
112
+
113
+ container.querySelectorAll('.document-chunk').forEach(chunk => {
114
+ if (!chunksWithBlocks.has(chunk)) targets.push(chunk);
115
+ });
116
+
117
+ return targets;
118
+ }
119
+
120
+ function drawHighlights(hlList, container, svg, charMap, fullText, utf8Map) {
121
+ const startTime = performance.now();
122
+ const svgNS = "http://www.w3.org/2000/svg";
123
+ svg.innerHTML = '';
124
+
125
+ // If called without pre-computed data, compute it
126
+ if (!charMap) {
127
+ const textNodes = collectTextNodes(container);
128
+ const built = buildCharMap(textNodes);
129
+ fullText = built.fullText;
130
+ charMap = built.charMap;
131
+ utf8Map = buildUTF8ToCharMap(fullText);
132
+ }
133
+
134
+ const containerRect = container.getBoundingClientRect();
135
+
136
+ // Batch: create all ranges and read all rects in one pass (single reflow)
137
+ const hlRanges = [];
138
+ for (const hl of hlList) {
139
+ if (hl.start_offset === undefined || hl.end_offset === undefined) continue;
140
+ const startChar = getCharIndexFromUTF8Index(fullText, hl.start_offset, utf8Map);
141
+ const endChar = getCharIndexFromUTF8Index(fullText, hl.end_offset, utf8Map);
142
+ if (startChar >= charMap.length || endChar <= 0 || startChar >= endChar) continue;
143
+
144
+ const clampedStart = Math.max(0, startChar);
145
+ const clampedEnd = Math.min(charMap.length, endChar);
146
+ const startEntry = charMap[clampedStart];
147
+ const endEntry = charMap[clampedEnd - 1];
148
+ if (!startEntry || !endEntry) continue;
149
+
150
+ try {
151
+ const range = document.createRange();
152
+ range.setStart(startEntry.node, startEntry.offset);
153
+ range.setEnd(endEntry.node, endEntry.offset + 1);
154
+ hlRanges.push({ highlight: hl, range });
155
+ } catch (e) {
156
+ // skip invalid ranges
157
+ }
158
+ }
159
+
160
+ // Single reflow: read all client rects
161
+ const allRects = [];
162
+ for (const { highlight, range } of hlRanges) {
163
+ const clientRects = range.getClientRects();
164
+ const raw = [];
165
+ for (const r of clientRects) {
166
+ if (r.width > 0 && r.height > 0) {
167
+ raw.push({
168
+ x: r.left - containerRect.left,
169
+ y: r.top - containerRect.top,
170
+ width: r.width,
171
+ height: r.height
172
+ });
173
+ }
174
+ }
175
+ const rects = deduplicateRects(raw);
176
+ for (const rect of rects) {
177
+ allRects.push({ highlight, rect, stackIndex: 0 });
178
+ }
179
+ }
180
+
181
+ const lineGroups = new Map();
182
+ allRects.forEach(item => {
183
+ const lineIndex = Math.floor(item.rect.y / 25);
184
+ if (!lineGroups.has(lineIndex)) lineGroups.set(lineIndex, []);
185
+ lineGroups.get(lineIndex).push(item);
186
+ });
187
+
188
+ lineGroups.forEach(rectsInLine => assignSmartLanes(rectsInLine));
189
+
190
+ // Batch DOM writes: build fragment then append once
191
+ const fragment = document.createDocumentFragment();
192
+ let lineCount = 0;
193
+ let gradientCount = 0;
194
+
195
+ allRects.forEach(item => {
196
+ const { highlight, rect, stackIndex } = item;
197
+ const tags = highlight.tags.map(tagId => findTag(tagId));
198
+ const colors = tags.map(tag => tag.color);
199
+ const lineY = rect.y + rect.height + BASE_OFFSET + (stackIndex * (LINE_HEIGHT + LINE_GAP));
200
+
201
+ if (colors.length === 1) {
202
+ const line = createSVGLine(rect.x, lineY, rect.x + rect.width, colors[0], highlight, false);
203
+ line.setAttribute("opacity", 0.9);
204
+ fragment.appendChild(line);
205
+ } else {
206
+ const group = document.createElementNS(svgNS, "g");
207
+ const gradientId = `gradient-${lineCount}-${gradientCount++}`;
208
+ group.appendChild(createSVGGradient(gradientId, colors));
209
+
210
+ const line = createSVGLine(rect.x, lineY, rect.x + rect.width, `url(#${gradientId})`, highlight, true);
211
+ line.setAttribute("opacity", 0.9);
212
+ group.appendChild(line);
213
+ fragment.appendChild(group);
214
+ }
215
+ lineCount++;
216
+ });
217
+
218
+ svg.appendChild(fragment);
219
+ svg.setAttribute('height', container.scrollHeight);
220
+ svg.style.height = container.scrollHeight + 'px';
221
+ log(`renderHighlights: ${((performance.now() - startTime) / 1000).toFixed(3)}s, ${lineCount} lines`);
222
+ }
223
+
224
+ function collectTextNodes(container) {
225
+ const nodes = [];
226
+ const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null);
227
+ let node;
228
+ while (node = walker.nextNode()) nodes.push(node);
229
+ return nodes;
230
+ }
231
+
232
+ function buildCharMap(textNodes) {
233
+ let fullText = '';
234
+ const charMap = [];
235
+ textNodes.forEach(node => {
236
+ const text = node.textContent;
237
+ for (let i = 0; i < text.length; i++) {
238
+ charMap.push({ node, offset: i });
239
+ }
240
+ fullText += text;
241
+ });
242
+ return { fullText, charMap };
243
+ }
244
+
245
+ function deduplicateRects(rects) {
246
+ // Remove exact duplicates
247
+ const unique = [];
248
+ for (const r of rects) {
249
+ const isDupe = unique.some(u =>
250
+ Math.abs(u.x - r.x) < 1 && Math.abs(u.y - r.y) < 1 &&
251
+ Math.abs(u.width - r.width) < 1 && Math.abs(u.height - r.height) < 1
252
+ );
253
+ if (!isDupe) unique.push(r);
254
+ }
255
+
256
+ // 2. Remove block-level rects (taller than text rects)
257
+ const medianHeight = unique.length > 0
258
+ ? unique.map(r => r.height).sort((a, b) => a - b)[Math.floor(unique.length / 2)]
259
+ : 20;
260
+ const result = [];
261
+ for (const r of unique) {
262
+ if (r.height > medianHeight * 1.3) {
263
+ const hasOverlapping = unique.some(other => {
264
+ if (other === r) return false;
265
+ return Math.abs(r.y - other.y) < r.height &&
266
+ other.height <= medianHeight * 1.1;
267
+ });
268
+ if (hasOverlapping) continue; // skip this block-level rect
269
+ }
270
+ result.push(r);
271
+ }
272
+
273
+ // Merge adjacent rects on the same visual line
274
+ result.sort((a, b) => a.y - b.y || a.x - b.x);
275
+ const merged = [];
276
+ for (const rect of result) {
277
+ const last = merged[merged.length - 1];
278
+ if (last && Math.abs(last.y - rect.y) < 3) {
279
+ const gap = rect.x - (last.x + last.width);
280
+ if (gap < 2) {
281
+ last.width = Math.max(last.x + last.width, rect.x + rect.width) - last.x;
282
+ last.height = Math.max(last.height, rect.height);
283
+ continue;
284
+ }
285
+ }
286
+ merged.push({ ...rect });
287
+ }
288
+ return merged;
289
+ }
290
+
291
+ function assignSmartLanes(rectsInLine) {
292
+ const lanes = [];
293
+ rectsInLine.sort((a, b) => a.rect.x - b.rect.x);
294
+
295
+ for (const current of rectsInLine) {
296
+ let placed = false;
297
+ for (let laneIdx = 0; laneIdx < lanes.length; laneIdx++) {
298
+ const overlaps = lanes[laneIdx].some(other => {
299
+ if (other.highlight.id === current.highlight.id) return false;
300
+ return !(current.rect.x >= other.rect.x + other.rect.width ||
301
+ other.rect.x >= current.rect.x + current.rect.width);
302
+ });
303
+ if (!overlaps) {
304
+ lanes[laneIdx].push(current);
305
+ current.stackIndex = laneIdx;
306
+ placed = true;
307
+ break;
308
+ }
309
+ }
310
+ if (!placed) {
311
+ current.stackIndex = lanes.length;
312
+ lanes.push([current]);
313
+ }
314
+ }
315
+ return lanes;
316
+ }
317
+
318
+ function createSVGLine(x1, y, x2, color, highlight, isGradient) {
319
+ const svgNS = "http://www.w3.org/2000/svg";
320
+ const line = document.createElementNS(svgNS, "line");
321
+
322
+ line.setAttribute("x1", x1);
323
+ line.setAttribute("y1", y);
324
+ line.setAttribute("x2", x2);
325
+ line.setAttribute("y2", y + (isGradient ? 0.1 : 0));
326
+ line.setAttribute("stroke", color);
327
+ line.setAttribute("stroke-width", LINE_HEIGHT);
328
+ line.setAttribute("stroke-linecap", "round");
329
+ line.setAttribute("data-highlight-id", highlight.id);
330
+ line.style.pointerEvents = 'all';
331
+ line.style.cursor = 'pointer';
332
+ line.style.transition = 'stroke-width 0.2s';
333
+
334
+ line.addEventListener('mouseenter', function(e) {
335
+ line.setAttribute("stroke-width", "5");
336
+ showLineTooltip(e, highlight.id);
337
+ });
338
+ line.addEventListener('mouseleave', function() {
339
+ line.setAttribute("stroke-width", LINE_HEIGHT);
340
+ hideLineTooltip();
341
+ });
342
+ line.addEventListener('click', function() {
343
+ editHighlight.call(line);
344
+ });
345
+
346
+ return line;
347
+ }
348
+
349
+ function createSVGGradient(id, colors) {
350
+ const svgNS = "http://www.w3.org/2000/svg";
351
+ const gradient = document.createElementNS(svgNS, "linearGradient");
352
+ gradient.setAttribute("id", id);
353
+ gradient.setAttribute("x1", "0%");
354
+ gradient.setAttribute("y1", "0%");
355
+ gradient.setAttribute("x2", "100%");
356
+ gradient.setAttribute("y2", "0%");
357
+
358
+ colors.forEach((color, index) => {
359
+ const stop = document.createElementNS(svgNS, "stop");
360
+ stop.setAttribute("offset", colors.length === 1 ? "100%" : `${(index / (colors.length - 1)) * 100}%`);
361
+ stop.setAttribute("stop-color", color);
362
+ gradient.appendChild(stop);
363
+ });
364
+ return gradient;
365
+ }
366
+
367
+ function showLineTooltip(event, highlightId) {
368
+ let tooltip = document.getElementById('highlight-tooltip');
369
+
370
+ if (!tooltip) {
371
+ tooltip = document.createElement('div');
372
+ tooltip.id = 'highlight-tooltip';
373
+ document.body.appendChild(tooltip);
374
+ Object.assign(tooltip.style, {
375
+ position: 'fixed', padding: '10px', background: '#fff',
376
+ border: '2px solid #333', borderRadius: '6px',
377
+ boxShadow: '0 4px 12px rgba(0,0,0,0.2)', fontSize: '13px',
378
+ pointerEvents: 'none', zIndex: '10000', maxWidth: '300px',
379
+ wordWrap: 'break-word', overflowWrap: 'break-word'
380
+ });
381
+ }
382
+
383
+ const hl = highlights[highlightId];
384
+ if (!hl) return;
385
+
386
+ const tags = hl.tags.map(tagId => findTag(tagId));
387
+ tooltip.innerHTML = `
388
+ <div style="display: flex; gap: 4px; flex-wrap: wrap; max-width: 280px;">
389
+ ${tags.map(tag => `
390
+ <div style="display: inline-block; padding: 4px 8px; border-radius: 3px;
391
+ background-color: ${tag.color}; color: white; font-size: 11px;">
392
+ ${tag.path || tag.name}
393
+ </div>
394
+ `).join('')}
395
+ </div>
396
+ `;
397
+ tooltip.style.display = 'block';
398
+ tooltip.style.left = (event.clientX + 10) + 'px';
399
+ tooltip.style.top = (event.clientY + 10) + 'px';
400
+ }
401
+
402
+ function hideLineTooltip() {
403
+ const tooltip = document.getElementById('highlight-tooltip');
404
+ if (tooltip) tooltip.style.display = 'none';
405
+ }
@@ -0,0 +1,15 @@
1
+ $(document).ready(function() {
2
+ $('#entity_id-select').hierarchySelect({
3
+ hierarchy: false,
4
+ search: true,
5
+ width: 'auto',
6
+ initialValueSet: true
7
+ });
8
+
9
+ $("#cilogon").click(function(){
10
+ let entity_id = encodeURIComponent($("#entity_id").val()),
11
+ url = $(this).attr('href').split('?')[0],
12
+ url_with_params = `${url}?entity_id=${entity_id}`;
13
+ $(this).attr('href', url_with_params);
14
+ });
15
+ })
@@ -0,0 +1,153 @@
1
+ /*
2
+ * Members
3
+ */
4
+
5
+ function addMember(login, privileges) {
6
+ members[login] = {privileges: privileges};
7
+ }
8
+
9
+ function removeMember(login) {
10
+ delete members[login];
11
+ }
12
+
13
+ let members_initial = {};
14
+ let members_displayed = {};
15
+
16
+ function _memberRow(login, user) {
17
+ let elem = document.createElement('div');
18
+ elem.className = 'row members-item';
19
+ elem.innerHTML = '<div class="col-md-4 text-center">' +
20
+ ' <i class="fas fa-user fa-2x"></i>' +
21
+ ' <p class="members-item-login">' + login + '</p>' +
22
+ '</div>' +
23
+ '<div class="col-md-4 form-group">' +
24
+ ' <select class="form-control">' +
25
+ ' <option value="PROJECT_OWNER">' + gettext("Can create projects") + '</option>' +
26
+ ' <option value="TEAM_MEMBER">' + gettext("Can't create projects") + '</option>' +
27
+ ' </select>' +
28
+ '</div>' +
29
+ '<hr class="custom-hr" />';
30
+
31
+ [].forEach.call(elem.querySelectorAll('option'), function (e) {
32
+ if (e.value === user.privileges) {
33
+ e.selected = true;
34
+ }
35
+ });
36
+
37
+ return elem;
38
+ }
39
+
40
+ function showMembers() {
41
+ document.getElementById('members-add').reset();
42
+ document.getElementById('members-add-fields').removeAttribute('disabled');
43
+
44
+ let entries = Object.entries(members);
45
+ sortByKey(entries, function (e) {
46
+ return e[0];
47
+ });
48
+ log("Members:", entries.map(function (e) {
49
+ return e[0] + " (" + e[1].privileges + ")";
50
+ })
51
+ .join(", "));
52
+
53
+ // Empty the list
54
+ let current_members = document.getElementById('members-current');
55
+ current_members.innerHTML = '';
56
+
57
+ // Fill it back up
58
+ for (let i = 0; i < entries.length; ++i) {
59
+ const login = entries[i][0];
60
+ const user = entries[i][1];
61
+ const elem = _memberRow(login, user);
62
+ if(login !== user_login) {
63
+ current_members.appendChild(elem);
64
+ }
65
+ }
66
+
67
+ // Store current state so that we can compare later
68
+ members_initial = Object.assign({}, members);
69
+ members_displayed = Object.assign({}, members);
70
+ }
71
+
72
+ document.getElementById('members-add').addEventListener('submit', function (e) {
73
+ e.preventDefault();
74
+
75
+ const login = document.getElementById('member-add-name').value.toLowerCase();
76
+ if (!login) {
77
+ return;
78
+ }
79
+ const privileges = document.getElementById('member-add-privileges').value;
80
+
81
+ // Check login
82
+ if (login in members_displayed) {
83
+ showAlert(gettext("Already a member!"));
84
+ document.getElementById('members-add').reset();
85
+ return;
86
+ }
87
+ postJSON('/api/check_user', {login: login, permission: true})
88
+ .then(function (result) {
89
+ if (result.exists) {
90
+ // Add it at the top
91
+ const elem = _memberRow(login, {privileges: privileges});
92
+ const current_members = document.getElementById('members-current');
93
+ current_members.insertBefore(elem, current_members.firstChild);
94
+ members_displayed[login] = true;
95
+
96
+ document.getElementById('members-add').reset();
97
+ } else {
98
+ showAlert(gettext("Cannot add this user<br/>Reasons : <ul><li>User cannot be found</li><li>User is already sponsored</li><li>User role cannot be changed</li></ul>"));
99
+ }
100
+ })
101
+ });
102
+
103
+ function sendMembersPatch() {
104
+ let patch = {};
105
+
106
+ let members_before = Object.assign({}, members_initial);
107
+
108
+ const rows = document.getElementById('members-current').querySelectorAll('.members-item');
109
+ for (let i = 0; i < rows.length; ++i) {
110
+ const row = rows[i];
111
+ const login = row.querySelector('.members-item-login').textContent;
112
+ const privileges = row.querySelector('select').value;
113
+
114
+ // Add to patch, if different from stored version
115
+ if (!members_before[login] || members_before[login].privileges !== privileges) {
116
+ patch[login] = {privileges: privileges};
117
+ }
118
+
119
+ // Remove from object, so what's left are the removed members
120
+ delete members_before[login];
121
+ }
122
+
123
+ // Remove the members that are left
124
+ const entries = Object.entries(members_before);
125
+ for (let i = 0; i < entries.length; ++i) {
126
+ patch[entries[i][0]] = null;
127
+ }
128
+
129
+ log("Patching users list");
130
+ showSpinner();
131
+ patchJSON('/api/permissions', patch)
132
+ .then(function () {
133
+ log("Users list patched");
134
+ })
135
+ .catch(function (error) {
136
+ log("Failed to patch users list", 'error', {error});
137
+ const updatePermissionsErrorMessage = gettext("Couldn't update permissions!");
138
+ showAlert(`${updatePermissionsErrorMessage} \n \n`, 'error', {error})
139
+ })
140
+ .then(hideSpinner);
141
+
142
+ members_initial = {};
143
+ }
144
+
145
+ document.getElementById('members-submit').addEventListener('click', function (e) {
146
+ e.preventDefault();
147
+ sendMembersPatch();
148
+ });
149
+
150
+ document.getElementById('members-current').addEventListener('submit', function (e) {
151
+ e.preventDefault();
152
+ sendMembersPatch();
153
+ });