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/main.py ADDED
@@ -0,0 +1,649 @@
1
+ import argparse
2
+ import asyncio
3
+ import base64
4
+ import gettext
5
+ import locale
6
+ import logging
7
+ import os
8
+ import pkg_resources
9
+ import prometheus_client
10
+ import re
11
+ import secrets
12
+ import string
13
+ import subprocess
14
+ import sys
15
+ import tornado.ioloop
16
+ from tornado.httpserver import HTTPServer
17
+ from tornado.netutil import bind_unix_socket
18
+ from urllib.parse import urlparse
19
+ import webbrowser
20
+
21
+ import taguette
22
+ from . import exact_version
23
+ from . import database
24
+ from .web import make_app
25
+ from scripts.make_theme import compile_scss
26
+
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ PROM_VERSION = prometheus_client.Gauge(
31
+ "version", "Application version", ["version"]
32
+ )
33
+
34
+
35
+ def prepare_db(database):
36
+ # Windows paths kinda look like URLs, but aren't
37
+ if sys.platform == "win32" and re.match(r"^[a-zA-Z]:\\", database):
38
+ logger.info("Database URL recognized as Windows path")
39
+ url = None
40
+ else:
41
+ url = urlparse(database)
42
+ if url is not None and url.scheme:
43
+ # Full URL: use it, create path if sqlite
44
+ db_url = database
45
+ if url.scheme == "sqlite" and url.path.startswith("/"):
46
+ os.makedirs(os.path.dirname(url.path[1:]), exist_ok=True)
47
+ else:
48
+ # Path: create it, turn into URL
49
+ database = os.path.expanduser(database)
50
+ if os.path.dirname(database):
51
+ os.makedirs(os.path.dirname(database), exist_ok=True)
52
+ db_url = "sqlite:///" + database
53
+ logger.info("Turning database path into URL: %s", db_url)
54
+ return db_url
55
+
56
+
57
+ def get_join_url(app):
58
+ url = "http://localhost"
59
+ if app.config["PORT"] != 80:
60
+ url += ":%d" % app.config["PORT"]
61
+ if app.config["BASE_PATH"] not in ("", "/"):
62
+ url += "/" + app.config["BASE_PATH"].strip("/")
63
+ url += "/"
64
+ token = app.single_user_token
65
+ if token:
66
+ url += "?token=%s" % token
67
+ return url
68
+
69
+
70
+ def default_config(output):
71
+ if output is None:
72
+ out = sys.stdout
73
+ else:
74
+ out = open(output, "w")
75
+ out.write(
76
+ '''\
77
+ # This is the configuration file for LibreQDA
78
+ # It is a Python file, so you can use the full Python syntax
79
+
80
+ # Name of this server
81
+ NAME = "Misconfigured LibreQDA Server"
82
+ # THEME = "default"
83
+
84
+ # Alternate templates
85
+ USE_ALTERNATE_WELCOME = False
86
+
87
+ # Address and port to listen on
88
+ BIND_ADDRESS = "0.0.0.0"
89
+ PORT = 7465
90
+
91
+ # Disable access to LibreQQA in case of maintenance
92
+ MAINTENANCE_MODE = False
93
+ MAINTENANCE_MESSAGE = ""
94
+
95
+ # Pull messages to print after login, or hardcode them.
96
+ CHECK_MESSAGES = False
97
+
98
+ # Push messages shown as banners on every page for logged-in users.
99
+ # Precedence: alt_templates/{{THEME}}/messages.json > MESSAGES_FILE
100
+ # > HARDCODED_MESSAGE > MESSAGES_URL.
101
+ # The JSON file is hot-reloaded on mtime change (no restart needed).
102
+ #MESSAGES_FILE = "messages.json"
103
+
104
+ #HARDCODED_MESSAGE = "[...]"
105
+ # Or with HTML formatting:
106
+ #HARDCODED_MESSAGE = {{
107
+ # "text": "without HTML formatting",
108
+ # "html": "with <strong>HTML</strong>] formatting"
109
+ #}}
110
+
111
+ #MESSAGES_URL = "https://yourdomain.com/api/messages"
112
+
113
+
114
+ # Base path of the application
115
+ BASE_PATH = ""
116
+
117
+ # A unique secret key that will be used to sign cookies
118
+ SECRET_KEY = "{secret}"
119
+
120
+ # Database to use
121
+ # This is a SQLAlchemy connection URL; refer to their documentation for info
122
+ # https://docs.sqlalchemy.org/en/latest/core/engines.html
123
+ # If using SQLite3 on Unix, note the 4 slashes for an absolute path
124
+ # (keep 3 before a relative path)
125
+ DATABASE = "sqlite:////non/existent/taguette/database.sqlite3"
126
+
127
+ # Redis instance for live collaboration
128
+ # This is not required if using a single server, collaboration will still work
129
+ #REDIS_SERVER = 'redis://localhost:6379'
130
+
131
+ # Address to send system emails from
132
+ EMAIL = "Misconfigured LibreQDA Server <LibreQDA@example.com>"
133
+
134
+ # Recipient for bug reports (defaults to EMAIL if unset)
135
+ #BUG_REPORT_EMAIL_TO = "bugs@example.com"
136
+
137
+ # Terms of service (HTML file)
138
+ TOS_FILE = 'tos.html'
139
+ # If set to None, no terms of service link will be displayed anywhere
140
+ #TOS_FILE = None
141
+
142
+ # Extra footer at the bottom of every page
143
+ #EXTRA_FOOTER = """
144
+ # | This instance of LibreQDA is managed by Example University.
145
+ # Please <a href="mailto:it@example.org">email IT</a> with any questions.
146
+ #"""
147
+
148
+ # Default language
149
+ DEFAULT_LANGUAGE = 'en_US'
150
+
151
+ # SMTP server to use to send emails
152
+ MAIL_SERVER = {{
153
+ "ssl": False,
154
+ "host": "localhost",
155
+ "port": 25,
156
+ }}
157
+
158
+ CHECK_MESSAGES = False
159
+
160
+ #BUG_REPORT_URL = {{
161
+ # "fr_FR": "",
162
+ # "en_US": "",
163
+ #}}
164
+
165
+ # Whether users must explicitly accept cookies before using the website
166
+ COOKIES_PROMPT = False
167
+
168
+ # Whether new users can create an account
169
+ REGISTRATION_ENABLED = True
170
+
171
+ # Whether to show credits container on index page
172
+ SHOW_CREDITS = False
173
+
174
+ # Whether users can import projects from SQLite3 files
175
+ SQLITE3_IMPORT_ENABLED = True
176
+
177
+ # Set this to true if you are behind a reverse proxy that sets the
178
+ # X-Forwarded-For header.
179
+ # Leave this at False if users are connecting to LibreQDA directly
180
+ X_HEADERS = False
181
+
182
+ # Time limits for converting documents
183
+ CONVERT_TO_HTML_TIMEOUT = 3 * 60 # 3min for importing document into LibreQDA
184
+ CONVERT_FROM_HTML_TIMEOUT = 3 * 60 # 3min for exporting from LibreQDA
185
+
186
+ # If you want to export metrics using Prometheus, set a port number here
187
+ #PROMETHEUS_LISTEN = "0.0.0.0:9101"
188
+
189
+ # If you want to report errors to Sentry, set your DSN here
190
+ #SENTRY_DSN = "https://<key>@sentry.io/<project>"
191
+
192
+ # If you want to enable multi-user app
193
+ # MULTIUSER = True
194
+
195
+ # Allow external authentication, MULTIUSER must be set to True
196
+ # If True, OAUTH variables must be defined
197
+ ALLOW_EXTERNAL_LOGIN = False
198
+ # For testing purpose only.
199
+ # To bypass OAUTH and to use built-in login and register
200
+ # URLs are : /login?bypass=true or /register?bypass=true
201
+ ALLOW_EXTERNAL_LOGIN_BYPASS = False
202
+
203
+ # Whitelist and Blacklist rules file location (see access_rules.example.json)
204
+ # ACCESS_RULES_FILE=None
205
+
206
+ # Replace current authentication with OAUTH
207
+ # OAUTH_AUTH_URI=""
208
+ # OAUTH_TOKEN_URI=""
209
+ # OAUTH_TOKEN_METHOD="get"
210
+ # OAUTH_USERINFO_URI=""
211
+ # OAUTH_CLIENT_ID=""
212
+ # OAUTH_CLIENT_SECRET=""
213
+ # OAUTH_SCOPE=[]
214
+ # OAUTH_CALLBACK_URL="http://localhost:7465/ci_callback"
215
+
216
+ # Output manifest max size
217
+ # OPF_OUT_SIZE_LIMIT = 5000000
218
+
219
+ # Max size of output file
220
+ # HTML_OUT_SIZE_LIMIT = 5000000
221
+ '''.format(secret=base64.b64encode(os.urandom(30)).decode("ascii"))
222
+ )
223
+ if output is not None:
224
+ out.close()
225
+
226
+
227
+ DEFAULT_CONFIG = {
228
+ "MULTIUSER": True,
229
+ "ALLOW_EXTERNAL_LOGIN": False,
230
+ "ALLOW_EXTERNAL_LOGIN_BYPASS": False,
231
+ "BIND_ADDRESS": "0.0.0.0",
232
+ "BASE_PATH": "",
233
+ "SHOW_CREDITS": True,
234
+ "THEME": "default",
235
+ "MAINTENANCE_MODE": False,
236
+ "MAINTENANCE_MESSAGE": "",
237
+ "BUG_REPORT_URL": None,
238
+ "BUG_REPORT_EMAIL_TO": None,
239
+ "INSTALLER_URL": None,
240
+ "MACOS_INSTALLER_URL": None,
241
+ "EXTRA_FOOTER": None,
242
+ "REGISTRATION_ENABLED": True,
243
+ "REDIS_SERVER": None,
244
+ "SQLITE3_IMPORT_ENABLED": True,
245
+ "DEFAULT_LANGUAGE": "en_US",
246
+ "CONVERT_FROM_HTML_TIMEOUT": 3 * 60,
247
+ "CONVERT_TO_HTML_TIMEOUT": 3 * 60,
248
+ "OPF_OUT_SIZE_LIMIT": 5000000, # 5 MB
249
+ "HTML_OUT_SIZE_LIMIT": 2000000, # 2 MB
250
+ 'LOKI_URL': 'http://localhost:3100',
251
+ 'UNIX_SOCKET': None,
252
+ 'ACCESS_RULES_FILE': None,
253
+ }
254
+
255
+ REQUIRED_CONFIG = [
256
+ "NAME",
257
+ "PORT",
258
+ "SECRET_KEY",
259
+ "DATABASE",
260
+ "TOS_FILE",
261
+ "X_HEADERS",
262
+ "EMAIL",
263
+ "MAIL_SERVER",
264
+ "COOKIES_PROMPT",
265
+ ]
266
+
267
+
268
+ async def _set_password(user):
269
+ import getpass
270
+
271
+ try:
272
+ passwd = getpass.getpass("Enter password for user %r: " % user.login)
273
+ except (OSError, EOFError):
274
+ character_set = (
275
+ string.ascii_lowercase + string.ascii_uppercase + string.digits
276
+ )
277
+ passwd = "".join(secrets.choice(character_set) for _ in range(16))
278
+ logger.warning(
279
+ "Can't prompt for a password, setting admin password to: %s",
280
+ passwd,
281
+ )
282
+ await user.set_password(passwd)
283
+
284
+
285
+ def main():
286
+ logging.root.handlers.clear()
287
+ logging.basicConfig(level=logging.INFO,
288
+ format="%(asctime)s %(levelname)s: %(message)s")
289
+
290
+ locale.setlocale(locale.LC_ALL, '')
291
+ lang = locale.getlocale()[0]
292
+ lang = [lang] if lang else []
293
+ d = pkg_resources.resource_filename("taguette", "l10n")
294
+ trans = gettext.translation("taguette_main", d, lang, fallback=True)
295
+ taguette._trans = trans
296
+ _ = trans.gettext
297
+
298
+ if sys.platform == "win32" and sys.version_info >= (3, 8):
299
+ # https://github.com/tornadoweb/tornado/issues/2608
300
+ try:
301
+ from asyncio import WindowsSelectorEventLoopPolicy
302
+ except ImportError:
303
+ pass
304
+ else:
305
+ policy = asyncio.get_event_loop_policy()
306
+ if not isinstance(policy, WindowsSelectorEventLoopPolicy):
307
+ asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy())
308
+
309
+ toplevel = os.path.dirname(os.path.dirname(__file__))
310
+ if os.path.exists(os.path.join(toplevel, ".git")):
311
+ try:
312
+ version = (
313
+ subprocess.check_output(
314
+ ["git", "--git-dir=.git", "describe", "--tags"],
315
+ cwd=toplevel,
316
+ stderr=subprocess.PIPE,
317
+ )
318
+ .decode("utf-8")
319
+ .strip()
320
+ )
321
+ except (OSError, subprocess.CalledProcessError):
322
+ logger.info(
323
+ "Can't get version from Git, using version=%s", exact_version()
324
+ )
325
+ else:
326
+ logger.info(
327
+ "Running from Git repository, using version=%s", version
328
+ )
329
+ if version.startswith("v"):
330
+ taguette._exact_version = version[1:]
331
+ if version.startswith("libreqda-v"):
332
+ taguette._exact_version = version[10:]
333
+ else:
334
+ logger.info("Not a Git repository, using version=%s", exact_version())
335
+ PROM_VERSION.labels(exact_version()).set(1)
336
+
337
+ if sys.platform == "win32":
338
+ import ctypes.wintypes
339
+
340
+ CSIDL_PERSONAL = 5 # My Documents
341
+ SHGFP_TYPE_CURRENT = 0 # Get current, not default value
342
+
343
+ buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH)
344
+ ctypes.windll.shell32.SHGetFolderPathW(
345
+ None, CSIDL_PERSONAL, None, SHGFP_TYPE_CURRENT, buf
346
+ )
347
+
348
+ default_db = os.path.join(buf.value, "Taguette", "taguette.sqlite3")
349
+ default_db_show = os.path.join(
350
+ os.path.basename(buf.value), "Taguette", "taguette.sqlite3"
351
+ )
352
+ else:
353
+ data = os.environ.get("XDG_DATA_HOME")
354
+ if not data:
355
+ data = os.path.join(os.environ["HOME"], ".local", "share")
356
+ default_db_show = "$HOME/.local/share/taguette/taguette.sqlite3"
357
+ else:
358
+ default_db_show = "$XDG_DATA_HOME/taguette/taguette.sqlite3"
359
+ default_db = os.path.join(data, "taguette", "taguette.sqlite3")
360
+
361
+ parser = argparse.ArgumentParser(
362
+ description="Document tagger for qualitative analysis",
363
+ )
364
+ parser.add_argument(
365
+ "--version",
366
+ action="version",
367
+ version="LibreQDA version %s" % exact_version(),
368
+ )
369
+ parser.add_argument(
370
+ "-p",
371
+ "--port",
372
+ default="7465",
373
+ help=_("Port number on which to listen"),
374
+ )
375
+ parser.add_argument(
376
+ "-b", "--bind", default="127.0.0.1", help=_("Address to bind on")
377
+ )
378
+ parser.add_argument(
379
+ "--browser",
380
+ action="store_true",
381
+ default=True,
382
+ help=_("Open web browser to the application"),
383
+ )
384
+ parser.add_argument(
385
+ "--no-browser",
386
+ action="store_false",
387
+ dest="browser",
388
+ help=_("Don't open the web browser"),
389
+ )
390
+ parser.add_argument(
391
+ "--debug", action="store_true", default=False, help=argparse.SUPPRESS
392
+ )
393
+ parser.add_argument(
394
+ "--database",
395
+ action="store",
396
+ default=default_db,
397
+ help=_(
398
+ "Database location or connection string, for "
399
+ "example 'project.db' or "
400
+ "'postgresql://me:pw@localhost/mydb' "
401
+ "(default: %(default)r)"
402
+ )
403
+ % dict(default=default_db_show),
404
+ )
405
+ parser.add_argument(
406
+ "--set-umask",
407
+ action="store",
408
+ dest="umask",
409
+ default="077",
410
+ help=_(
411
+ "Set the file creation mask (umask)on systems that support it."
412
+ ),
413
+ )
414
+ parser.add_argument(
415
+ "--dont-set-umask",
416
+ action="store_const",
417
+ dest="umask",
418
+ const=None,
419
+ help=_("Don't change umask on startup"),
420
+ )
421
+ parser.add_argument('--base_path', default='',
422
+ help=_("Base path for the URL"))
423
+ parser.add_argument('--unix_socket', default=None,
424
+ help=_("Unix socket to bind to instead of port"))
425
+ parser.set_defaults(
426
+ func=None, # Function to run instead of default, e.g. migrate, server
427
+ func1=None, # Run immediately, e.g. before anything is logged
428
+ )
429
+
430
+ subparsers = parser.add_subparsers(
431
+ title=_("additional commands"), metavar="", dest="cmd"
432
+ )
433
+
434
+ parser_migrate = subparsers.add_parser(
435
+ "migrate", help=_("Manually trigger a database migration")
436
+ )
437
+ parser_migrate.add_argument(
438
+ "revision", action="store", default="head", nargs=argparse.OPTIONAL
439
+ )
440
+ parser_migrate.set_defaults(
441
+ func=lambda args: database.migrate(
442
+ prepare_db(args.database),
443
+ args.revision,
444
+ )
445
+ )
446
+
447
+ parser_config = subparsers.add_parser(
448
+ "default-config", help=_("Print the default server configuration")
449
+ )
450
+ parser_config.add_argument(
451
+ "--output",
452
+ "-o",
453
+ action="store",
454
+ nargs=1,
455
+ help=_("Output to this file rather than stdout"),
456
+ )
457
+ parser_config.set_defaults(func1=lambda args: default_config(args.output))
458
+
459
+ parser_server = subparsers.add_parser(
460
+ "server",
461
+ help=_("Run in server mode, suitable for a multi-user deployment"),
462
+ )
463
+ parser_server.add_argument(
464
+ "config_file",
465
+ help=_(
466
+ "Configuration file for the server. The "
467
+ "default configuration can be generated "
468
+ "using the `default-config` command"
469
+ ),
470
+ )
471
+
472
+ args = parser.parse_args()
473
+
474
+ if args.func1:
475
+ args.func1(args)
476
+ sys.exit(0)
477
+
478
+ if args.umask is not None:
479
+ if not re.match(r"^[0-7][0-7][0-7]$", args.umask):
480
+ print(
481
+ _("Invalid umask: %(arg)s") % dict(arg=args.umask),
482
+ file=sys.stderr,
483
+ flush=True,
484
+ )
485
+ sys.exit(2)
486
+ logger.info("Setting umask to %s", args.umask)
487
+ os.umask(int(args.umask, 8))
488
+
489
+ if args.func:
490
+ args.func(args)
491
+ sys.exit(0)
492
+
493
+ if args.cmd == "server":
494
+ # Set configuration from config file
495
+ config = {}
496
+ config_file = os.path.abspath(args.config_file)
497
+ with open(config_file) as fp:
498
+ code = compile(fp.read(), config_file, "exec")
499
+ exec(code, config)
500
+ config = dict(DEFAULT_CONFIG, **config)
501
+ missing = False
502
+ for key in REQUIRED_CONFIG:
503
+ if key not in config:
504
+ print(
505
+ _("Missing required configuration variable %(var)s")
506
+ % dict(var=key),
507
+ file=sys.stderr,
508
+ flush=True,
509
+ )
510
+ missing = True
511
+ if missing:
512
+ sys.exit(2)
513
+ if config["BASE_PATH"] and config["BASE_PATH"][0] != "/":
514
+ print(_("Invalid BASE_PATH"))
515
+ config["BASE_PATH"] = config["BASE_PATH"].strip("/")
516
+ if config["BASE_PATH"]:
517
+ config["BASE_PATH"] = "/" + config["BASE_PATH"]
518
+ else:
519
+ if args.debug:
520
+ # Use a deterministic secret key, to avoid it changing during
521
+ # auto-reload and such
522
+ secret = "debug"
523
+ else:
524
+ secret = os.urandom(30).decode("iso-8859-15")
525
+
526
+ # Set configuration from command-line
527
+ config = dict(
528
+ DEFAULT_CONFIG,
529
+ MULTIUSER=False,
530
+ BIND_ADDRESS=args.bind,
531
+ X_HEADERS=False,
532
+ SQLITE3_IMPORT_ENABLED=True,
533
+ PORT=int(args.port),
534
+ DATABASE=prepare_db(args.database),
535
+ TOS_FILE=None,
536
+ SECRET_KEY=secret,
537
+ COOKIES_PROMPT=False,
538
+ HTML_OUT_SIZE_LIMIT=5000000, # 5 MB
539
+ BASE_PATH=args.base_path,
540
+ UNIX_SOCKET=args.unix_socket,
541
+ )
542
+
543
+ # Access rules
544
+ access_rules_file = config.get('ACCESS_RULES_FILE')
545
+ if access_rules_file:
546
+ from taguette.access_control import load_rules
547
+ config['_access_rules'] = load_rules(access_rules_file)
548
+ logger.info("Access rules loaded from %s", access_rules_file)
549
+ else:
550
+ config['_access_rules'] = None
551
+
552
+ # Loki integration
553
+ try:
554
+ from taguette.loki_handler import setup_loki_logging
555
+ loki_url = config.get('LOKI_URL', DEFAULT_CONFIG['LOKI_URL'])
556
+ logger.info("Loki URL: %s", loki_url)
557
+ setup_loki_logging(loki_url)
558
+ logger.info("Loki handler configured")
559
+ except ImportError:
560
+ logger.warning("loki_handler module not available")
561
+
562
+ if 'PROMETHEUS_LISTEN' in config:
563
+ p_addr = None
564
+ p_port = config["PROMETHEUS_LISTEN"]
565
+ if isinstance(p_port, str):
566
+ if ":" in p_port:
567
+ p_addr, p_port = p_port.split(":")
568
+ p_addr = p_addr or None
569
+ p_port = int(p_port)
570
+ logger.info("Starting Prometheus exporter on port %d", p_port)
571
+ prometheus_client.start_http_server(p_port, p_addr)
572
+
573
+ if "SENTRY_DSN" in config:
574
+ import sentry_sdk
575
+ from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration
576
+ from sentry_sdk.integrations.tornado import TornadoIntegration
577
+
578
+ logger.info("Initializing Sentry")
579
+ sentry_sdk.init(
580
+ dsn=config["SENTRY_DSN"],
581
+ integrations=[TornadoIntegration(), SqlalchemyIntegration()],
582
+ ignore_errors=[KeyboardInterrupt],
583
+ release="taguette@%s" % exact_version(),
584
+ )
585
+
586
+ xsrf_cookies = True
587
+ if config["ALLOW_EXTERNAL_LOGIN_BYPASS"]:
588
+ xsrf_cookies = False
589
+ app = make_app(config, debug=args.debug, xsrf_cookies=xsrf_cookies)
590
+ if args.unix_socket:
591
+ server = HTTPServer(app)
592
+ socket = bind_unix_socket(args.unix_socket)
593
+ server.add_socket(socket)
594
+ else:
595
+ app.listen(
596
+ config["PORT"],
597
+ address=config["BIND_ADDRESS"],
598
+ xheaders=config.get("X_HEADERS", False),
599
+ )
600
+ loop = tornado.ioloop.IOLoop.current()
601
+
602
+ db = app.DBSession()
603
+ admin = db.query(database.User).get("admin")
604
+ if admin is None:
605
+ logger.warning("Creating user 'admin'")
606
+ admin = database.User(login="admin")
607
+ if config["MULTIUSER"]:
608
+ loop.run_sync(lambda: _set_password(admin))
609
+ db.add(admin)
610
+ db.commit()
611
+ elif config["MULTIUSER"] and not admin.hashed_password:
612
+ loop.run_sync(lambda: _set_password(admin))
613
+ db.commit()
614
+ db.close()
615
+
616
+ if args.debug:
617
+ logger.warning("Debug mode is ON")
618
+ asyncio.get_event_loop().set_debug(True)
619
+
620
+ if config['THEME']:
621
+ logger.info("Recompiling SCSS!")
622
+ compile_scss(config['THEME'])
623
+
624
+ if args.unix_socket:
625
+ print(
626
+ _(
627
+ "\n Taguette %(version)s is now running using "
628
+ "UNIX socket %(socket)s") %
629
+ dict(socket=args.unix_socket, version=exact_version()),
630
+ flush=True
631
+ )
632
+ else:
633
+ url = get_join_url(app)
634
+ print(
635
+ _(
636
+ "\n Taguette %(version)s is now running. You can "
637
+ "connect to it using this link:\n\n %(url)s\n") %
638
+ dict(url=url, version=exact_version()),
639
+ flush=True
640
+ )
641
+
642
+ if args.browser and not args.debug:
643
+ loop.call_later(0.01, webbrowser.open, url)
644
+
645
+ loop.start()
646
+
647
+
648
+ if __name__ == "__main__":
649
+ main()