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
taguette/export.py ADDED
@@ -0,0 +1,500 @@
1
+ import asyncio
2
+ import contextlib
3
+ import csv
4
+ import jinja2
5
+ import pkg_resources
6
+ from markupsafe import Markup
7
+ import opentelemetry.trace
8
+ from sqlalchemy import not_, func, distinct, or_
9
+ from sqlalchemy import and_, select, exists
10
+ from sqlalchemy.orm import joinedload
11
+ import uuid
12
+ import xlsxwriter
13
+ from xml.sax.saxutils import XMLGenerator
14
+ from xml.sax.xmlreader import AttributesNSImpl
15
+
16
+ from . import exact_version
17
+ from . import convert
18
+ from . import database
19
+ from . import extract
20
+ from .database.models import collection_documents
21
+ from .utils import get_path_informations
22
+
23
+ tracer = opentelemetry.trace.get_tracer(__name__)
24
+
25
+ template_env = jinja2.Environment(
26
+ loader=jinja2.FileSystemLoader(
27
+ [pkg_resources.resource_filename('taguette', 'templates')]
28
+ ),
29
+ autoescape=jinja2.select_autoescape(['html']),
30
+ extensions=['jinja2.ext.i18n'],
31
+ )
32
+
33
+
34
+ def _render_string(template_name, locale, **kwargs):
35
+ translator = _Translator(locale)
36
+ template = template_env.get_template(template_name)
37
+ return template.render(
38
+ version=exact_version(),
39
+ gettext=translator.gettext,
40
+ ngettext=translator.ngettext,
41
+ pgettext=translator.pgettext,
42
+ **kwargs,
43
+ )
44
+
45
+
46
+ class _Translator(object):
47
+ def __init__(self, locale):
48
+ self.locale = locale
49
+
50
+ def gettext(self, message, **kwargs):
51
+ trans = self.locale.translate(message)
52
+ if kwargs:
53
+ trans = trans % kwargs
54
+ return trans
55
+
56
+ def ngettext(self, singular, plural, n, **kwargs):
57
+ trans = self.locale.translate(singular, plural, n)
58
+ if kwargs:
59
+ trans = trans % kwargs
60
+ return trans
61
+
62
+ def pgettext(
63
+ self,
64
+ context, message, plural_message=None,
65
+ n=None,
66
+ **kwargs,
67
+ ):
68
+ trans = self.locale.pgettext(context, message, plural_message, n)
69
+ if kwargs:
70
+ trans = trans % kwargs
71
+ return trans
72
+
73
+
74
+ def _get_highlights_for_export(db, project_id, paths):
75
+ list_of_paths, operator_type, exclude_paths = get_path_informations(paths)
76
+
77
+ t_highlight = database.Highlight.__table__
78
+ t_highlight_tag = database.highlight_tags
79
+ t_tag = database.Tag.__table__
80
+ t_document = database.Document.__table__
81
+ t_collection_documents = collection_documents
82
+ t_collection = database.Collection.__table__
83
+ t_highlight_tag_m = t_highlight_tag.alias()
84
+ t_tag_m = t_tag.alias()
85
+
86
+ # Base query setup
87
+ query = select([
88
+ t_highlight.c.id,
89
+ t_highlight.c.snippet,
90
+ t_document.c.name,
91
+ t_tag.c.path,
92
+ t_collection.c.name.label('collection_name'),
93
+ ]).select_from(
94
+ t_highlight
95
+ .join(t_highlight_tag,
96
+ t_highlight.c.id == t_highlight_tag.c.highlight_id)
97
+ .join(t_tag, t_tag.c.id == t_highlight_tag.c.tag_id)
98
+ .join(t_document, t_document.c.id == t_highlight.c.document_id)
99
+ .outerjoin(t_collection_documents,
100
+ t_document.c.id == t_collection_documents.c.document_id)
101
+ .outerjoin(t_collection,
102
+ t_collection.c.id == t_collection_documents.c.collection_id)
103
+ ).where(t_document.c.project_id == project_id)
104
+
105
+ if operator_type == 'or' and any(list_of_paths):
106
+ query = query.join(
107
+ t_highlight_tag_m,
108
+ t_highlight.c.id == t_highlight_tag_m.c.highlight_id
109
+ ).join(
110
+ t_tag_m, t_tag_m.c.id == t_highlight_tag_m.c.tag_id
111
+ ).filter(
112
+ t_tag_m.c.path.in_(list_of_paths)
113
+ )
114
+
115
+ elif operator_type == 'and' and any(list_of_paths):
116
+ highlight_ids_with_all_tags = select(
117
+ [t_highlight_tag.c.highlight_id]
118
+ ).join(
119
+ t_tag,
120
+ t_tag.c.id == t_highlight_tag.c.tag_id
121
+ ).where(
122
+ and_(
123
+ t_tag.c.path.in_(
124
+ list_of_paths), t_tag.c.project_id == project_id
125
+ )
126
+ ).group_by(
127
+ t_highlight_tag.c.highlight_id
128
+ ).having(
129
+ func.count(distinct(t_tag.c.id)) == len(list_of_paths)
130
+ ).alias()
131
+
132
+ query = query.join(highlight_ids_with_all_tags,
133
+ t_highlight.c.id ==
134
+ highlight_ids_with_all_tags.c.highlight_id)
135
+ elif operator_type == 'mixed' and any(list_of_paths):
136
+ or_conditions = []
137
+
138
+ for group in list_of_paths:
139
+ if len(group) == 1:
140
+ or_conditions.append(t_tag.c.path == group[0])
141
+ else:
142
+ highlight_ids_with_all_tags = select(
143
+ [t_highlight_tag.c.highlight_id]) \
144
+ .join(t_tag, t_tag.c.id == t_highlight_tag.c.tag_id) \
145
+ .filter(
146
+ t_tag.c.path.in_(group),
147
+ t_tag.c.project_id == project_id) \
148
+ .group_by(t_highlight_tag.c.highlight_id) \
149
+ .having(func.count(distinct(t_tag.c.id)) == len(group)) \
150
+ .alias("and_subquery_for_group")
151
+
152
+ or_conditions.append(
153
+ t_highlight.c.id.in_(
154
+ select([highlight_ids_with_all_tags.c.highlight_id])))
155
+
156
+ if or_conditions:
157
+ query = query.filter(or_(*or_conditions))
158
+ else:
159
+ # Special case to select all highlights: we also need to select
160
+ # highlights that have no tag at all, so the startswith() condition
161
+ # would not work
162
+ pass
163
+
164
+ # Handle exclusion of paths
165
+ if any(exclude_paths):
166
+ exclude_subquery = select([t_highlight_tag.c.highlight_id]) \
167
+ .join(t_tag, t_tag.c.id == t_highlight_tag.c.tag_id) \
168
+ .where(and_(t_tag.c.path.in_(exclude_paths),
169
+ t_tag.c.project_id == project_id)).alias()
170
+
171
+ query = query.where(
172
+ not_(
173
+ exists().where(
174
+ t_highlight.c.id == exclude_subquery.c.highlight_id
175
+ )
176
+ )
177
+ )
178
+
179
+ query = query.order_by(
180
+ t_highlight.c.document_id,
181
+ t_highlight.c.start_offset,
182
+ t_highlight.c.id,
183
+ )
184
+
185
+ # Collect highlights
186
+ highlights = []
187
+ for row in db.execute(query).fetchall():
188
+ highlight_id, snippet, document, tag_path, collection_name = row
189
+
190
+ # Build full collection path if collection exists
191
+ collection_path = ''
192
+ if collection_name:
193
+ collection_path = _build_collection_path(
194
+ db, collection_name, project_id)
195
+
196
+ if highlights and highlights[-1][0] == highlight_id:
197
+ if tag_path not in highlights[-1][3]:
198
+ highlights[-1][3].append(tag_path)
199
+ else:
200
+ highlights.append((highlight_id, snippet, document, [
201
+ tag_path] if tag_path else [], collection_path))
202
+
203
+ return highlights
204
+
205
+
206
+ def _build_collection_path(db, collection_name, project_id):
207
+ """Build the full path of a collection including all parents.
208
+
209
+ Returns a string like "Parent > Child > Grandchild" or just "Collection"
210
+ if no parents.
211
+ """
212
+ t_collection = database.Collection.__table__
213
+
214
+ # Find the collection by name and project
215
+ collection_query = select([
216
+ t_collection.c.id,
217
+ t_collection.c.name,
218
+ t_collection.c.parent_id
219
+ ]).where(
220
+ and_(
221
+ t_collection.c.name == collection_name,
222
+ t_collection.c.project_id == project_id
223
+ )
224
+ )
225
+
226
+ collection_row = db.execute(collection_query).fetchone()
227
+ if not collection_row:
228
+ return collection_name
229
+
230
+ _, name, parent_id = collection_row
231
+
232
+ # Build path by traversing up the hierarchy
233
+ path_parts = [name]
234
+ current_parent_id = parent_id
235
+
236
+ while current_parent_id:
237
+ parent_query = select([
238
+ t_collection.c.name,
239
+ t_collection.c.parent_id
240
+ ]).where(
241
+ t_collection.c.id == current_parent_id
242
+ )
243
+ parent_row = db.execute(parent_query).fetchone()
244
+
245
+ if parent_row:
246
+ parent_name, current_parent_id = parent_row
247
+ path_parts.insert(0, parent_name)
248
+ else:
249
+ break
250
+
251
+ return ' > '.join(path_parts)
252
+
253
+
254
+ def get_filename_for_highlights_export(path):
255
+ """Generates an appropriate filename for
256
+ exporting highlights based on tags.
257
+ """
258
+ if path:
259
+ filename = (path.replace('∪', '_or_')
260
+ .replace('∩', '_and_')
261
+ .replace('¬', '_not_'))
262
+ filename = filename.replace(' ', '').lower()
263
+ max_length = 255
264
+ if len(filename) > max_length:
265
+ filename = filename[:max_length]
266
+ return filename
267
+ else:
268
+ return 'all_tags'
269
+
270
+
271
+ @tracer.start_as_current_span('taguette/export/highlights_csv')
272
+ def highlights_csv(db, project_id, path, file):
273
+ """Export highlights to a CSV file.
274
+ """
275
+ highlights = _get_highlights_for_export(db, project_id, path)
276
+ writer = csv.writer(file)
277
+ writer.writerow(['id', 'document', 'collection', 'tag', 'content'])
278
+ for id, snippet, document, tags, collection in highlights:
279
+ if not tags:
280
+ tags = ['']
281
+ for tag_path in tags:
282
+ writer.writerow([
283
+ id, document, collection, tag_path,
284
+ convert.html_to_plaintext(snippet),
285
+ ])
286
+
287
+
288
+ @tracer.start_as_current_span('taguette/export/highlights_xlsx')
289
+ def highlights_xslx(db, project_id, path, filename):
290
+ """Export highlights to an Excel file.
291
+ """
292
+ highlights = _get_highlights_for_export(db, project_id, path)
293
+
294
+ workbook = xlsxwriter.Workbook(filename)
295
+ sheet = workbook.add_worksheet('highlights')
296
+
297
+ header = workbook.add_format({'bold': True})
298
+
299
+ sheet.write(0, 0, 'id', header)
300
+ sheet.write(0, 1, 'document', header)
301
+ sheet.write(0, 2, 'collection', header)
302
+ sheet.write(0, 3, 'tag', header)
303
+ sheet.write(0, 4, 'content', header)
304
+ sheet.set_column(0, 0, 5.0)
305
+ sheet.set_column(1, 1, 15.0)
306
+ sheet.set_column(2, 2, 15.0)
307
+ sheet.set_column(3, 3, 15.0)
308
+ sheet.set_column(4, 4, 80.0)
309
+ row = 1
310
+ for id, snippet, document, tags, collection in highlights:
311
+ if not tags:
312
+ tags = ['']
313
+ for tag_path in tags:
314
+ sheet.write(row, 0, str(id))
315
+ sheet.write(row, 1, document)
316
+ sheet.write(row, 2, collection)
317
+ sheet.write(row, 3, tag_path)
318
+ sheet.write(row, 4, convert.html_to_plaintext(snippet))
319
+ row += 1
320
+ workbook.close()
321
+
322
+
323
+ def format_title(path):
324
+ formatted_path = (path.replace('∩', ' ∩ ')
325
+ .replace('∪', ' ∪ ')
326
+ .replace('¬', ' ! '))
327
+ return formatted_path
328
+
329
+
330
+ @tracer.start_as_current_span('taguette/export/highlights_doc')
331
+ def highlights_doc(db, project_id, path, ext, *, config, locale):
332
+ """Export highlights to a text document.
333
+ """
334
+ highlights = _get_highlights_for_export(db, project_id, path)
335
+
336
+ formatted_path = format_title(path)
337
+
338
+ html = _render_string(
339
+ 'export_highlights.html',
340
+ locale,
341
+ path=formatted_path,
342
+ highlights=highlights,
343
+ )
344
+
345
+ mimetype, contents = convert.html_to(html, ext, config)
346
+ return mimetype, contents
347
+
348
+
349
+ @tracer.start_as_current_span('taguette/export/highlighted_document')
350
+ async def highlighted_document(db, document, ext, *, config, locale):
351
+ """Export a document annotated with highlights.
352
+
353
+ Each highlight is followed by its tags in brackets.
354
+ """
355
+ highlights = (
356
+ db.query(database.Highlight)
357
+ .filter(database.Highlight.document_id == document.id)
358
+ .order_by(database.Highlight.start_offset)
359
+ .options(joinedload(database.Highlight.tags))
360
+ ).all()
361
+
362
+ highlights = [
363
+ (hl.start_offset, hl.end_offset, [t.path for t in hl.tags])
364
+ for hl in highlights
365
+ ]
366
+
367
+ html = await asyncio.get_event_loop().run_in_executor(
368
+ None,
369
+ lambda: extract.highlight(
370
+ document.contents, highlights,
371
+ show_tags=True,
372
+ ),
373
+ )
374
+
375
+ html = _render_string(
376
+ 'export_document.html',
377
+ locale,
378
+ name=document.name,
379
+ contents=Markup(html),
380
+ )
381
+
382
+ mimetype, contents = convert.html_to(
383
+ html, ext,
384
+ config,
385
+ )
386
+ contents = await contents
387
+ return mimetype, contents
388
+
389
+
390
+ TAGUETTE_NAMESPACE = uuid.UUID('51B2B2B7-27EB-4ECB-9D56-E75B0A0496C2')
391
+
392
+
393
+ @tracer.start_as_current_span('taguette/export/codebook_xml')
394
+ def codebook_xml(tags, file):
395
+ """Export a codebook in REFI-QDA format for the given tags.
396
+ """
397
+ with contextlib.ExitStack() as stack:
398
+ if not hasattr(file, 'write'):
399
+ file = stack.enter_context(open(file, 'wb'))
400
+
401
+ # http://schema.qdasoftware.org/versions/Codebook/v1.0/Codebook.xsd
402
+ output = XMLGenerator(
403
+ file,
404
+ encoding='utf-8',
405
+ short_empty_elements=True,
406
+ )
407
+ output.startDocument()
408
+ output.startPrefixMapping(None, 'urn:QDA-XML:codebook:1.0')
409
+ output.startElementNS(
410
+ (None, 'CodeBook'), 'CodeBook',
411
+ AttributesNSImpl(
412
+ {(None, 'origin'): 'Taguette %s' % exact_version()},
413
+ {(None, 'origin'): 'origin'},
414
+ ),
415
+ )
416
+ output.startElementNS(
417
+ (None, 'Codes'), 'Codes',
418
+ AttributesNSImpl({}, {}),
419
+ )
420
+ for tag in tags:
421
+ guid = uuid.uuid5(TAGUETTE_NAMESPACE, tag.path)
422
+ guid = str(guid).upper()
423
+ output.startElementNS(
424
+ (None, 'Code'), 'Code',
425
+ AttributesNSImpl({(None, 'guid'): guid,
426
+ (None, 'name'): tag.path,
427
+ (None, 'isCodable'): 'true'},
428
+ {(None, 'guid'): 'guid',
429
+ (None, 'name'): 'name',
430
+ (None, 'isCodable'): 'isCodable'}),
431
+ )
432
+ output.endElementNS((None, 'Code'), 'Code')
433
+ output.endElementNS((None, 'Codes'), 'Codes')
434
+ output.startElementNS(
435
+ (None, 'Sets'), 'Sets',
436
+ AttributesNSImpl({}, {}),
437
+ )
438
+ output.endElementNS((None, 'Sets'), 'Sets')
439
+ output.endElementNS((None, 'CodeBook'), 'CodeBook')
440
+ output.endPrefixMapping(None)
441
+ output.endDocument()
442
+
443
+
444
+ @tracer.start_as_current_span('taguette/export/codebook_csv')
445
+ def codebook_csv(tags, file):
446
+ """Export a codebook in CSV format for the given tags.
447
+ """
448
+ with contextlib.ExitStack() as stack:
449
+ if not hasattr(file, 'write'):
450
+ file = stack.enter_context(open(file, 'w'))
451
+
452
+ writer = csv.writer(file)
453
+ writer.writerow(['tag', 'description', 'color',
454
+ 'parent', 'number of highlights']
455
+ )
456
+ for tag in tags:
457
+ writer.writerow([tag.path, tag.description, tag.color,
458
+ tag.parent.path if tag.parent else '',
459
+ tag.highlights_count])
460
+
461
+
462
+ @tracer.start_as_current_span('taguette/export/codebook_xlsx')
463
+ def codebook_xlsx(tags, filename):
464
+ """Export a codebook in Excel format for the given tags.
465
+ """
466
+ workbook = xlsxwriter.Workbook(filename)
467
+ sheet = workbook.add_worksheet('codebook')
468
+
469
+ header = workbook.add_format({'bold': True})
470
+
471
+ sheet.write(0, 0, 'tag', header)
472
+ sheet.write(0, 1, 'description', header)
473
+ sheet.write(0, 2, 'parent', header)
474
+ sheet.write(0, 3, 'number of highlights', header)
475
+ sheet.set_column(0, 0, 30.0)
476
+ sheet.set_column(1, 1, 80.0)
477
+ for row, tag in enumerate(tags, start=1):
478
+ sheet.write(row, 0, tag.path)
479
+ sheet.write(row, 1, tag.description)
480
+ sheet.write(row, 2, tag.parent.path if tag.parent else '')
481
+ sheet.write(row, 3, tag.highlights_count)
482
+ workbook.close()
483
+
484
+
485
+ @tracer.start_as_current_span('taguette/export/codebook_document')
486
+ async def codebook_document(tags, ext, *, config, locale):
487
+ """Export a codebook as a text document for the given tags.
488
+ """
489
+ html = _render_string(
490
+ 'export_codebook.html',
491
+ locale,
492
+ tags=tags,
493
+ )
494
+
495
+ mimetype, contents = convert.html_to(
496
+ html, ext,
497
+ config,
498
+ )
499
+ contents = await contents
500
+ return mimetype, contents