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,49 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}{% trans "page title" %}Import codebook{% endtrans %} | LibreQDA{% endblock %}
4
+
5
+ {% block contents %}
6
+ <h1>{% trans "title" %}Import a codebook{% endtrans %}</h1>
7
+ <p>{% trans %}Use this form to import an existing codebook into your project. Existing tags will not be deleted, new tags will be added alongside them.{% endtrans %}</p>
8
+ {% for error in errors %}
9
+ <div class="alert alert-danger">{{ error }}</div>
10
+ {% endfor %}
11
+ <p>{% trans %}You are about to create the following tags:{% endtrans %}</p>
12
+ <form action="{{ reverse_url('import_codebook', project.id) }}" method="post">
13
+ {{ xsrf_form_html() }}
14
+ {% for tag in tags %}
15
+ <div class="form-group row">
16
+ <div class="col-sm-2 form-check">
17
+ {% if tag.exists %}
18
+ <input type="checkbox" class="form-check-input" name="tag{{ loop.index0 }}-replace" id="tag{{ loop.index0 }}-replace" aria-label="Replace this tag"{% if tag.enabled %} checked{% endif %} />
19
+ <label for="tag{{ loop.index0 }}-replace">{% trans "codebook import tag option" %}Replace{% endtrans %}</label>
20
+ {% else %}
21
+ <input type="checkbox" class="form-check-input" name="tag{{ loop.index0 }}-import" id="tag{{ loop.index0 }}-import" aria-label="Replace this tag"{% if tag.enabled %} checked{% endif %} />
22
+ <label for="tag{{ loop.index0 }}-import">{% trans "codebook import tag option" %}Import{% endtrans %}</label>
23
+ {% endif %}
24
+ </div>
25
+ <div class="col-sm-3">
26
+ {% if tag.parent == '' %}
27
+ <input type="text" class="form-control" value="{{ tag.path }}" disabled/>
28
+ {% else %}
29
+ <input type="text" class="form-control" value="{{ tag.path }} -> {{ tag.parent }}" disabled/>
30
+ {% endif %}
31
+ <input type="hidden" name="tag{{ loop.index0 }}-path" value="{{ tag.path }}"/>
32
+ <input type="hidden" name="tag{{ loop.index0 }}-parent" value="{{ tag.parent }}"/>
33
+ </div>
34
+ <div class="col-sm-2">
35
+ <input type="color" class="form-control" name="tag{{ loop.index0 }}-color" value="{{ tag.color }}"/>
36
+ </div>
37
+ <div class="col-sm-5">
38
+ <input type="text" class="form-control" name="tag{{ loop.index0 }}-description" value="{{ tag.description }}" />
39
+ </div>
40
+ </div>
41
+ {% endfor %}
42
+ <div class="form-group row">
43
+ <div class="col-sm-10 offset-sm-2">
44
+ <input type="submit" class="btn btn-primary" value="{% trans "form submit button" %}Create tags{% endtrans %}" />
45
+ <a href="{{ reverse_url('project', project.id) }}" class="btn btn-secondary">{% trans "cancel button" %}Go back{% endtrans %}</a>
46
+ </div>
47
+ </div>
48
+ </form>
49
+ {% endblock %}
@@ -0,0 +1,31 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}{% trans "page title" %}New project{% endtrans %} | LibreQDA{% endblock %}
4
+
5
+ {% block contents %}
6
+ <p>{% trans "title" %}New project{% endtrans %}</p>
7
+ {% if error %}
8
+ <div class="alert alert-danger">{{ error }}</div>
9
+ {% endif %}
10
+ <form action="{{ reverse_url('new_project') }}" method="post">
11
+ {{ xsrf_form_html() }}
12
+ <div class="form-group row">
13
+ <label for="project-name" class="col-sm-2 col-form-label">{% trans "form label for new project name" %}Name{% endtrans %}</label>
14
+ <div class="col-sm-10">
15
+ <input type="text" class="form-control" id="project-name" name="name" value="{{ name }}" required />
16
+ </div>
17
+ </div>
18
+ <div class="form-group row">
19
+ <label for="project-description" class="col-sm-2 col-form-label">{% trans "form label for new project description" %}Description{% endtrans %}</label>
20
+ <div class="col-sm-10">
21
+ <textarea class="form-control" id="project-description" name="description">{{ description }}</textarea>
22
+ </div>
23
+ </div>
24
+ <div class="form-group row">
25
+ <div class="col-sm-10 offset-sm-2">
26
+ <button type="submit" class="btn btn-primary" data-test-id="save-project-button">{% trans "form submit button" %}Create{% endtrans %}</button>
27
+ <a href="{{ reverse_url('index') }}" class="btn btn-secondary ms-2">{% trans %}Cancel{% endtrans %}</a>
28
+ </div>
29
+ </div>
30
+ </form>
31
+ {% endblock %}
@@ -0,0 +1,8 @@
1
+ {% extends "base.html" %}
2
+ {% set active_page = "tos" %}
3
+
4
+ {% block title %}{% trans "page title" %}Terms of Service{% endtrans %} | LibreQDA{% endblock %}
5
+
6
+ {% block contents %}
7
+ {{ tos_text }}
8
+ {% endblock %}
@@ -0,0 +1,7 @@
1
+ var lang = {{ language }};
2
+ var catalog = {{ catalog }};
3
+
4
+ window.gettext = function(message, args) {
5
+ var trans = catalog[message] || message;
6
+ return trans.replace(/%\(([^)]+)\)s/g, function(match, name) { return args[name]; });
7
+ }
@@ -0,0 +1,15 @@
1
+ {% extends "base.html" %}
2
+ {% set active_page = "index" %}
3
+
4
+ {% block title %}{% trans "page title" %}Welcome{% endtrans %} | LibreQDA{% endblock %}
5
+
6
+ {% block contents %}
7
+ <div class="jumbotron">
8
+ <h1 class="display-3">{% trans "title" %}Welcome{% endtrans %}</h1>
9
+ <p class="lead">{% trans %}<span><a href="https://help.libreqda.org/">LibreQDA</a></span> is an open-source web-based document tagging tool for qualitative data analysis.{% endtrans %}</p>
10
+ <p>{% trans %}Using this tool, you can upload a collection of documents, create a hierarchy of codes, and annotate portions of documents with codes and notes that you can recall and organize later.{% endtrans %}</p>
11
+ {% if register_enabled %}
12
+ <p>{% trans url=reverse_url('register') %}<a href="{{ url }}">Register now</a> for free and get started!{% endtrans %}</p>
13
+ {% endif %}
14
+ </div>
15
+ {% endblock %}
taguette/utils.py ADDED
@@ -0,0 +1,177 @@
1
+ import asyncio
2
+ import logging
3
+ import os
4
+ import re
5
+ import sentry_sdk
6
+ import sys
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ def _f(message):
12
+ """Pass-through translation function.
13
+
14
+ Marks a string for translation without translating it at run time.
15
+ """
16
+ return message
17
+
18
+
19
+ class DefaultMap(object):
20
+ def __init__(self, default, mapping):
21
+ self.__default = default
22
+ self.mapping = mapping
23
+
24
+ def get(self, key):
25
+ try:
26
+ return self.mapping[key]
27
+ except KeyError:
28
+ return self.__default(key)
29
+
30
+ def __getitem__(self, key):
31
+ return self.get(key)
32
+
33
+
34
+ async def log_and_wait_proc(logger, proc):
35
+ while proc.returncode is None:
36
+ line = await proc.stdout.readline()
37
+ if not line:
38
+ break
39
+ line = line.decode('utf-8', 'replace')
40
+ if line[-1] == '\n':
41
+ line = line[:-1]
42
+ logger.info("%d: %s", proc.pid, line)
43
+
44
+ ret = await proc.wait()
45
+ lines = await proc.stdout.read()
46
+ for line in lines.splitlines():
47
+ line = line.decode('utf-8', 'replace')
48
+ logger.info("%d: %s", proc.pid, line)
49
+ return ret
50
+
51
+
52
+ _windows_device_files = ('CON', 'AUX', 'COM1', 'COM2', 'COM3', 'COM4', 'LPT1',
53
+ 'LPT2', 'LPT3', 'PRN', 'NUL')
54
+ _not_ascii_re = re.compile(r'[^A-Za-z0-9_.-]')
55
+ _percent_escapes_re = re.compile(r'%[0-9A-Fa-f]{2}')
56
+
57
+
58
+ def sanitize_filename(name):
59
+ """Sanitize a filename.
60
+
61
+ This takes a filename, for example provided by a browser with a file
62
+ upload, and turn it into something that is safe for opening.
63
+
64
+ Adapted from werkzeug's secure_filename(), copyright 2007 the Pallets team.
65
+ https://palletsprojects.com/p/werkzeug/
66
+ """
67
+ if not isinstance(name, str):
68
+ raise ValueError("File name is not a string")
69
+ if not name:
70
+ raise ValueError("File name cannot be empty")
71
+ name = _percent_escapes_re.sub('_', name)
72
+ if '/' in name:
73
+ name = name[name.rindex('/') + 1:]
74
+ if sanitize_filename.windows and '\\' in name:
75
+ # It seems that IE gets that wrong, at least when the file is from
76
+ # a network share
77
+ name = name[name.rindex('\\') + 1:]
78
+ name, ext = os.path.splitext(name)
79
+ name = name[:20]
80
+ ext = ext[:8]
81
+ name = _not_ascii_re.sub('', name).strip('._')
82
+ if not name:
83
+ name = '_'
84
+ ext = _not_ascii_re.sub('', ext)
85
+ if (
86
+ sanitize_filename.windows
87
+ and name.split('.')[0].upper() in _windows_device_files
88
+ ):
89
+ name = '_' + name
90
+ name = name + ext
91
+ return name
92
+
93
+
94
+ sanitize_filename.windows = os.name == 'nt'
95
+
96
+ _background_future_references = {}
97
+
98
+
99
+ def background_task(task, *, should_never_exit=False):
100
+ future = asyncio.get_event_loop().create_task(task)
101
+ ident = id(future)
102
+
103
+ def callback(future):
104
+ _background_future_references.pop(ident, None)
105
+ try:
106
+ future.result()
107
+ except Exception as e:
108
+ sentry_sdk.capture_exception(e)
109
+ logger.exception("Exception in background task")
110
+ if should_never_exit:
111
+ logger.critical("Critical task died, exiting")
112
+ asyncio.get_event_loop().stop()
113
+ sys.exit(1)
114
+
115
+ future.add_done_callback(callback)
116
+
117
+ # Keep a strong reference to the future
118
+ # https://bugs.python.org/issue21163
119
+ _background_future_references[ident] = future
120
+
121
+
122
+ def check_operator_consistency(paths):
123
+ has_and = '∩' in paths
124
+ has_or = '∪' in paths
125
+
126
+ if has_and and has_or:
127
+ return 'mixed'
128
+ elif has_and:
129
+ return 'and'
130
+ elif has_or:
131
+ return 'or'
132
+ else:
133
+ return 'or'
134
+
135
+
136
+ def parse_paths_for_exclusion(paths):
137
+ import re
138
+ exclude_paths = []
139
+ include_paths = []
140
+ for path in re.findall(r'([∩∪¬]?[^\∩∪¬]+)', paths):
141
+ if path.startswith('¬'):
142
+ exclude_paths.append(path[1:])
143
+ else:
144
+ include_paths.append(path)
145
+ include_paths_string = ''.join(include_paths)
146
+ return exclude_paths, include_paths_string
147
+
148
+
149
+ def get_path_informations(paths):
150
+ exclude_paths, include_paths_string = parse_paths_for_exclusion(paths)
151
+
152
+ operator_type = check_operator_consistency(paths)
153
+
154
+ list_of_paths = []
155
+
156
+ if operator_type == 'and':
157
+ list_of_paths = include_paths_string.split('∩')
158
+ elif operator_type == 'or':
159
+ list_of_paths = include_paths_string.split('∪')
160
+ elif operator_type == 'mixed':
161
+ list_of_paths = separate_and_group_tags(paths)
162
+
163
+ return list_of_paths, operator_type, exclude_paths
164
+
165
+
166
+ def separate_and_group_tags(paths):
167
+
168
+ exclude_paths, include_paths_string = parse_paths_for_exclusion(paths)
169
+
170
+ or_groups = include_paths_string.split('∪')
171
+
172
+ grouped_tags = []
173
+ for group in or_groups:
174
+ and_tags = group.split('∩')
175
+ grouped_tags.append(and_tags)
176
+
177
+ return grouped_tags
taguette/validate.py ADDED
@@ -0,0 +1,141 @@
1
+ import os
2
+ import string
3
+
4
+ from .utils import _f
5
+
6
+
7
+ class InvalidFormat(ValueError):
8
+ """User-supplied value doesn't pass validation."""
9
+
10
+ def __init__(self, message):
11
+ super(InvalidFormat, self).__init__(message)
12
+ self.message = message
13
+
14
+
15
+ def description(descr):
16
+ if not isinstance(descr, str):
17
+ raise ValueError("Description is not a string")
18
+ if len(descr) > 102400:
19
+ raise InvalidFormat(_f("Description is too long"))
20
+ return True
21
+
22
+
23
+ def color(value):
24
+ if not isinstance(value, str):
25
+ raise ValueError("color is not a string")
26
+ return True
27
+
28
+
29
+ def project_name(name):
30
+ if not name:
31
+ raise InvalidFormat(_f("Project name cannot be empty"))
32
+ if len(name) > 50:
33
+ raise InvalidFormat(_f("Project name is too long"))
34
+ return True
35
+
36
+
37
+ ALLOWED_LOGIN_CHARACTERS_NEW = (
38
+ string.ascii_lowercase + string.digits +
39
+ '._-'
40
+ )
41
+ ALLOWED_LOGIN_CHARACTERS = (
42
+ ALLOWED_LOGIN_CHARACTERS_NEW + '+@ '
43
+ )
44
+
45
+
46
+ def fix_user_login(login, *, new=False):
47
+ login = login.lower()
48
+ user_login(login, new=new)
49
+ return login
50
+
51
+
52
+ def user_login(login, *, new=False):
53
+ if not isinstance(login, str):
54
+ raise ValueError("Login is not a string")
55
+ if not login:
56
+ raise InvalidFormat(_f("User login cannot be empty"))
57
+ if len(login) > 255:
58
+ raise InvalidFormat(_f("User login is too long"))
59
+ if login != login.lower():
60
+ raise InvalidFormat(_f("User login is not lowercase"))
61
+ chars = ALLOWED_LOGIN_CHARACTERS_NEW if new else ALLOWED_LOGIN_CHARACTERS
62
+ if any(c not in chars for c in login):
63
+ raise InvalidFormat(_f("User login contains forbidden characters"))
64
+ return True
65
+
66
+
67
+ def user_email(email):
68
+ if not isinstance(email, str):
69
+ raise ValueError("Email is not a string")
70
+ if not email:
71
+ raise InvalidFormat(_f("Email cannot be empty")) # but it can be NULL
72
+ if '@' not in email:
73
+ raise InvalidFormat(_f("Invalid email address"))
74
+ if len(email) > 256:
75
+ raise InvalidFormat(_f("Email address is too long"))
76
+ return True
77
+
78
+
79
+ def user_password(password):
80
+ if not isinstance(password, str):
81
+ raise ValueError("Password is not a string")
82
+ if len(password) < 5:
83
+ raise InvalidFormat(_f("Please use a longer password"))
84
+ if len(password) > 5120:
85
+ raise InvalidFormat(_f("Please use a shorter password"))
86
+ return True
87
+
88
+
89
+ def document_name(name):
90
+ if not isinstance(name, str):
91
+ raise ValueError("Document name is not a string")
92
+ if not name:
93
+ raise InvalidFormat(_f("Document name cannot be empty"))
94
+ if len(name) > 50:
95
+ raise InvalidFormat(_f("Document name is too long"))
96
+ return True
97
+
98
+
99
+ def tag_path(path):
100
+ if not isinstance(path, str):
101
+ raise ValueError("Tag path is not a string")
102
+ if not path:
103
+ raise InvalidFormat(_f("Tag path cannot be empty"))
104
+ if len(path) > 200:
105
+ raise InvalidFormat(_f("Tag path is too long"))
106
+ return True
107
+
108
+
109
+ def filename(name):
110
+ if not isinstance(name, str):
111
+ raise ValueError("File name is not a string")
112
+ if not name:
113
+ raise ValueError("File name cannot be empty")
114
+ if len(name) > 100:
115
+ raise InvalidFormat(_f("File name is too long"))
116
+ if '\x00' in name:
117
+ raise InvalidFormat(_f("Invalid file name"))
118
+ return True
119
+
120
+
121
+ def fix_filename(name):
122
+ if not isinstance(name, str):
123
+ raise ValueError("File name is not a string")
124
+ if not name:
125
+ raise ValueError("File name cannot be empty")
126
+ if '/' in name:
127
+ name = name[name.rindex('/') + 1:]
128
+ if fix_filename.windows and '\\' in name:
129
+ # It seems that IE gets that wrong, at least when the file is from
130
+ # a network share
131
+ name = name[name.rindex('\\') + 1:]
132
+ name, ext = os.path.splitext(name)
133
+ name = name[:50]
134
+ ext = ext[:8]
135
+ if not name:
136
+ name = '_'
137
+ name = name + ext
138
+ return name
139
+
140
+
141
+ fix_filename.windows = os.name == 'nt'
@@ -0,0 +1,102 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ import re
5
+ import sys
6
+ import time
7
+
8
+ from tornado.httpclient import AsyncHTTPClient
9
+
10
+ from . import __version__
11
+ from .utils import background_task
12
+
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ GITLAB_TAGS_URL = (
18
+ 'https://gitlab.com/api/v4/projects/'
19
+ 'bin-cirst%2Flibreqda%2Flibre-qda/repository/tags'
20
+ '?per_page=20&order_by=updated&sort=desc'
21
+ )
22
+ CACHE_TTL = 6 * 3600
23
+
24
+ _VERSION_RE = re.compile(r'(\d+(?:\.\d+)*)')
25
+
26
+ _cache = {
27
+ 'latest': None,
28
+ 'checked_at': 0.0,
29
+ 'in_flight': False,
30
+ }
31
+
32
+
33
+ def _parse(version):
34
+ match = _VERSION_RE.search(version or '')
35
+ if not match:
36
+ return ()
37
+ return tuple(int(x) for x in match.group(1).split('.'))
38
+
39
+
40
+ def _is_newer(candidate, current):
41
+ try:
42
+ return _parse(candidate) > _parse(current)
43
+ except Exception:
44
+ return False
45
+
46
+
47
+ async def _fetch_latest():
48
+ try:
49
+ client = AsyncHTTPClient()
50
+ response = await client.fetch(
51
+ GITLAB_TAGS_URL,
52
+ request_timeout=5,
53
+ connect_timeout=5,
54
+ )
55
+ data = json.loads(response.body)
56
+ if data and isinstance(data, list):
57
+ latest = None
58
+ latest_parsed = ()
59
+ for tag in data:
60
+ name = tag.get('name') or ''
61
+ parsed = _parse(name)
62
+ if parsed and parsed > latest_parsed:
63
+ latest_parsed = parsed
64
+ latest = name
65
+ if latest:
66
+ _cache['latest'] = latest
67
+ logger.info("Latest LibreQDA tag: %s", latest)
68
+ except Exception as e:
69
+ logger.warning("Version check failed: %s", e)
70
+ finally:
71
+ _cache['checked_at'] = time.time()
72
+ _cache['in_flight'] = False
73
+
74
+
75
+ def is_desktop_bundle():
76
+ return getattr(sys, 'frozen', False)
77
+
78
+
79
+ def get_outdated_version():
80
+ forced = os.environ.get('LIBREQDA_FORCE_OUTDATED_VERSION')
81
+ if forced:
82
+ return forced
83
+
84
+ if not is_desktop_bundle():
85
+ return None
86
+
87
+ now = time.time()
88
+ if (
89
+ not _cache['in_flight']
90
+ and now - _cache['checked_at'] > CACHE_TTL
91
+ ):
92
+ _cache['in_flight'] = True
93
+ try:
94
+ background_task(_fetch_latest())
95
+ except RuntimeError:
96
+ _cache['in_flight'] = False
97
+
98
+ latest = _cache['latest']
99
+ if latest and _is_newer(latest, __version__):
100
+ match = _VERSION_RE.search(latest)
101
+ return match.group(1) if match else latest
102
+ return None