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.
- CHANGELOG.md +481 -0
- libreqda-1.2.0.dist-info/LICENSE.txt +28 -0
- libreqda-1.2.0.dist-info/METADATA +131 -0
- libreqda-1.2.0.dist-info/RECORD +255 -0
- libreqda-1.2.0.dist-info/WHEEL +4 -0
- libreqda-1.2.0.dist-info/entry_points.txt +4 -0
- scripts/babelrc +8 -0
- scripts/dist/LICENSE.txt +33 -0
- scripts/dist/RELEASE.txt +23 -0
- scripts/dist/macos/libreqda.icns +0 -0
- scripts/dist/macos/macos.spec +43 -0
- scripts/dist/macos/plist.patch +10 -0
- scripts/dist/macos/taguette.icns +0 -0
- scripts/dist/macos/taguette_console_wrapper +4 -0
- scripts/dist/macos.sh +32 -0
- scripts/dist/pyinstaller_entrypoint.py +19 -0
- scripts/dist/windows/windows-installer.iss +51 -0
- scripts/dist/windows/windows.spec +39 -0
- scripts/edugain/worker_update_edugain_idps.py +89 -0
- scripts/edugain/worker_update_edugain_idps.sh +9 -0
- scripts/make_password_reset_link.py +56 -0
- scripts/make_theme.py +99 -0
- scripts/new_db_revision.sh +8 -0
- scripts/register_user.py +76 -0
- scripts/update_pot.sh +33 -0
- scripts/update_translations.sh +17 -0
- scripts/validate.py +140 -0
- scripts/wipe_and_disable_user.py +90 -0
- taguette/__init__.py +13 -0
- taguette/access_control.py +145 -0
- taguette/convert.py +623 -0
- taguette/database/__init__.py +177 -0
- taguette/database/base.py +9 -0
- taguette/database/copy.py +433 -0
- taguette/database/models.py +920 -0
- taguette/demodata.py +688 -0
- taguette/export.py +500 -0
- taguette/extract.py +287 -0
- taguette/import_codebook.py +110 -0
- taguette/l10n/de_DE/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/de_DE/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/el_EL/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/el_GR/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/el_GR/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/en_US/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/en_US/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/es_ES/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/es_ES/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/fr_FR/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/fr_FR/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/id_ID/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/id_ID/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/it_IT/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/it_IT/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/nl_NL/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/nl_NL/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/pt_PT/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/pt_PT/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/l10n/sv_SE/LC_MESSAGES/taguette_javascript.mo +0 -0
- taguette/l10n/sv_SE/LC_MESSAGES/taguette_main.mo +0 -0
- taguette/loki_handler.py +107 -0
- taguette/main.py +649 -0
- taguette/migrations/env.py +72 -0
- taguette/migrations/script.py.mako +24 -0
- taguette/migrations/versions/09c662cd9483_make_documents_filename_not_null.py +39 -0
- taguette/migrations/versions/1b29e68900c1_add_user_roles.py +55 -0
- taguette/migrations/versions/1b96a606551d_update_user_to_have_a_display_name.py +34 -0
- taguette/migrations/versions/2467bab602b7_add_color_column_to_tags_table.py +33 -0
- taguette/migrations/versions/382db36f20f4_add_user_disabled.py +66 -0
- taguette/migrations/versions/43d6c240309d_remove_document_groups.py +81 -0
- taguette/migrations/versions/447d636f72c5_cascades.py +40 -0
- taguette/migrations/versions/491de2dc7cd7_add_indexes.py +51 -0
- taguette/migrations/versions/4c55b3b3ff30_user_login_length.py +58 -0
- taguette/migrations/versions/61a4abcc8769_user_with_parent_for_permissions.py +40 -0
- taguette/migrations/versions/6489b5f9cfb5_increase_password_hash_size.py +34 -0
- taguette/migrations/versions/679f625e6e6a_fix_unique_tags.py +59 -0
- taguette/migrations/versions/77c4543a5b9b_add_parent_id_column_to_collection_table.py +41 -0
- taguette/migrations/versions/7c94b7a31e69_add_is_archived_column_to_projets.py +39 -0
- taguette/migrations/versions/7f56de97e3c8_change_edugain_displayname_length.py +42 -0
- taguette/migrations/versions/807c0cc0ecf6_remove_invalid_hltags.py +43 -0
- taguette/migrations/versions/80b1cc9d4c22_add_user_email.py +35 -0
- taguette/migrations/versions/91ade71ccf4d_add_user_language.py +28 -0
- taguette/migrations/versions/955db2e523d1_add_collections_tables_and_associations_.py +60 -0
- taguette/migrations/versions/a1b2c3d4e5f6_add_user_hide_code_counts.py +28 -0
- taguette/migrations/versions/b23f3b7a638e_add_user_password_change_date.py +36 -0
- taguette/migrations/versions/b7c1e2d3f4a5_add_project_compact_layout.py +28 -0
- taguette/migrations/versions/bc8e0e0677e9_rename_command_fields.py +126 -0
- taguette/migrations/versions/bce44849c2f2_lowercase_logins.py +96 -0
- taguette/migrations/versions/ca076140922f_update_user_for_external_logins.py +32 -0
- taguette/migrations/versions/d86c45d7aa8a_add_document_direction.py +127 -0
- taguette/migrations/versions/db5e31a0233d_remove_blank_highlights.py +88 -0
- taguette/migrations/versions/dd37d9027a24_unique_tags.py +68 -0
- taguette/migrations/versions/de678cef2a94_add_project_id_to_collections_and_.py +41 -0
- taguette/migrations/versions/e459991d960e_add_table_edugainidps.py +39 -0
- taguette/migrations/versions/e4cf92942271_add_index.py +29 -0
- taguette/migrations/versions/e4e090a5b511_remove_commands_document_id_fk.py +38 -0
- taguette/migrations/versions/e9a9f1c77971_add_favorite_column_to_document.py +32 -0
- taguette/migrations/versions/ecb4065de575_don_t_use_server_default_now_timezone_.py +44 -0
- taguette/migrations/versions/fbdc29566f62_edugain_fix_field_type.py +42 -0
- taguette/migrations/versions/fdc6eb54b034_add_parent_id_column_to_tags_table.py +31 -0
- taguette/static/README.md +34 -0
- taguette/static/alt_templates/.gitkeep +0 -0
- taguette/static/assets/test/logos/monsters-university-logo.png +0 -0
- taguette/static/css/custom.css +1696 -0
- taguette/static/css/fa-all.min.css +5 -0
- taguette/static/css/themed.css +3 -0
- taguette/static/css/themed.css.map +25 -0
- taguette/static/css/vendor/bootstrap-treeview.min.css +1 -0
- taguette/static/css/vendor/bootstrap.min.css +6 -0
- taguette/static/css/vendor/font/context-menu-icons.eot +0 -0
- taguette/static/css/vendor/font/context-menu-icons.ttf +0 -0
- taguette/static/css/vendor/font/context-menu-icons.woff +0 -0
- taguette/static/css/vendor/font/context-menu-icons.woff2 +0 -0
- taguette/static/css/vendor/hierarchy-select.min.css +1 -0
- taguette/static/css/vendor/introjs.min.css +2 -0
- taguette/static/css/vendor/jquery.contextMenu.min.css +16 -0
- taguette/static/favicon.ico +0 -0
- taguette/static/js/codebook-viz.js +428 -0
- taguette/static/js/collection/context-menu.js +180 -0
- taguette/static/js/collection/hierarchy-select.js +71 -0
- taguette/static/js/collection/manager.js +217 -0
- taguette/static/js/document/context-menu.js +92 -0
- taguette/static/js/document/custom-select.js +82 -0
- taguette/static/js/document/manager.js +943 -0
- taguette/static/js/document/treeview.js +593 -0
- taguette/static/js/highlight/README.md +18 -0
- taguette/static/js/highlight/crud.js +119 -0
- taguette/static/js/highlight/extracts.js +497 -0
- taguette/static/js/highlight/loader.js +109 -0
- taguette/static/js/highlight/renderer.js +405 -0
- taguette/static/js/login.js +15 -0
- taguette/static/js/permissions.js +153 -0
- taguette/static/js/polling-event.js +145 -0
- taguette/static/js/project/intro.js +524 -0
- taguette/static/js/project/manager.js +0 -0
- taguette/static/js/project/search.js +124 -0
- taguette/static/js/tag/context-menu.js +133 -0
- taguette/static/js/tag/explore-codes.js +400 -0
- taguette/static/js/tag/hierarchy-select.js +69 -0
- taguette/static/js/tag/manager.js +545 -0
- taguette/static/js/tag/treeview.js +613 -0
- taguette/static/js/taguette.js +1790 -0
- taguette/static/js/utilities.js +714 -0
- taguette/static/js/vendor/bootstrap-treeview-custum.js +421 -0
- taguette/static/js/vendor/bootstrap-treeview-custum.min.js +1 -0
- taguette/static/js/vendor/bootstrap-treeview-default.min.js +1 -0
- taguette/static/js/vendor/bootstrap.bundle.min.js +7 -0
- taguette/static/js/vendor/bootstrap.bundle.min.js.map +1 -0
- taguette/static/js/vendor/hierarchy-select.min.js +1 -0
- taguette/static/js/vendor/introjs.min.js +17 -0
- taguette/static/js/vendor/jquery-3.7.1.js +2 -0
- taguette/static/js/vendor/jquery.contextMenu.min.js +2 -0
- taguette/static/libreqda.ico +0 -0
- taguette/static/logo-h.png +0 -0
- taguette/static/logo.png +0 -0
- taguette/static/logo_blanc.png +0 -0
- taguette/static/missing.png +0 -0
- taguette/static/robots.txt +2 -0
- taguette/static/scss/components/_badge.scss +42 -0
- taguette/static/scss/components/_button.scss +169 -0
- taguette/static/scss/components/_checkbox.scss +40 -0
- taguette/static/scss/components/_context_menu.scss +28 -0
- taguette/static/scss/components/_custom_select.scss +150 -0
- taguette/static/scss/components/_dropdown.scss +43 -0
- taguette/static/scss/components/_form.scss +54 -0
- taguette/static/scss/components/_modal.scss +57 -0
- taguette/static/scss/components/_project_card.scss +163 -0
- taguette/static/scss/components/_tag.scss +308 -0
- taguette/static/scss/components/_treeview.scss +76 -0
- taguette/static/scss/custom.scss +809 -0
- taguette/static/scss/layout/_credits.scss +62 -0
- taguette/static/scss/layout/_footer.scss +6 -0
- taguette/static/scss/layout/_navigation.scss +165 -0
- taguette/static/scss/layout/_sidebar.scss +37 -0
- taguette/static/scss/themes/default/_variables.scss +103 -0
- taguette/static/scss/themes/default/theme.css +3 -0
- taguette/static/scss/themes/default/theme.css.map +26 -0
- taguette/static/scss/themes/default/theme.scss +4 -0
- taguette/static/webfonts/BadScript-OFL.txt +94 -0
- taguette/static/webfonts/BadScript.ttf +0 -0
- taguette/static/webfonts/Montserrat-Light.ttf +0 -0
- taguette/static/webfonts/Montserrat-OFL.txt +93 -0
- taguette/static/webfonts/fa-brands-400.eot +0 -0
- taguette/static/webfonts/fa-brands-400.svg +1184 -0
- taguette/static/webfonts/fa-brands-400.ttf +0 -0
- taguette/static/webfonts/fa-brands-400.woff +0 -0
- taguette/static/webfonts/fa-brands-400.woff2 +0 -0
- taguette/static/webfonts/fa-regular-400.eot +0 -0
- taguette/static/webfonts/fa-regular-400.svg +467 -0
- taguette/static/webfonts/fa-regular-400.ttf +0 -0
- taguette/static/webfonts/fa-regular-400.woff +0 -0
- taguette/static/webfonts/fa-regular-400.woff2 +0 -0
- taguette/static/webfonts/fa-solid-900.eot +0 -0
- taguette/static/webfonts/fa-solid-900.svg +2618 -0
- taguette/static/webfonts/fa-solid-900.ttf +0 -0
- taguette/static/webfonts/fa-solid-900.woff +0 -0
- taguette/static/webfonts/fa-solid-900.woff2 +0 -0
- taguette/templates/alt_templates/.gitkeep +0 -0
- taguette/templates/archived_projects.html +50 -0
- taguette/templates/auth/account.html +74 -0
- taguette/templates/auth/cilogon.login.html +89 -0
- taguette/templates/auth/email_reset_password.html +5 -0
- taguette/templates/auth/external_account.html +60 -0
- taguette/templates/auth/login.html +144 -0
- taguette/templates/auth/reset_password.html +27 -0
- taguette/templates/auth/token_needed.html +7 -0
- taguette/templates/base.html +209 -0
- taguette/templates/bug_report.html +158 -0
- taguette/templates/cookies_prompt.html +15 -0
- taguette/templates/downloads.html +127 -0
- taguette/templates/email_bug_report.txt +9 -0
- taguette/templates/email_reset_password.txt +5 -0
- taguette/templates/error.html +8 -0
- taguette/templates/export_codebook.html +20 -0
- taguette/templates/export_document.html +20 -0
- taguette/templates/export_highlights.html +47 -0
- taguette/templates/index.html +347 -0
- taguette/templates/maintenance.html +8 -0
- taguette/templates/messages.js +74 -0
- taguette/templates/modal/change_document.html +68 -0
- taguette/templates/modal/create_collection.html +100 -0
- taguette/templates/modal/create_document.html +84 -0
- taguette/templates/modal/create_highlight.html +38 -0
- taguette/templates/modal/create_tag.html +73 -0
- taguette/templates/modal/explore_codes.html +58 -0
- taguette/templates/modal/manage_collaborators.html +44 -0
- taguette/templates/modal/merge_tag.html +49 -0
- taguette/templates/new_password.html +32 -0
- taguette/templates/partials/alert.html +16 -0
- taguette/templates/partials/codebook_viz_modal.html +34 -0
- taguette/templates/partials/confirm.html +17 -0
- taguette/templates/partials/document_toolbar.html +37 -0
- taguette/templates/partials/hlinfo.html +13 -0
- taguette/templates/partials/project_card.html +44 -0
- taguette/templates/partials/project_card_archived.html +41 -0
- taguette/templates/partials/project_sidebar.html +99 -0
- taguette/templates/partials/spinner.html +11 -0
- taguette/templates/permissions.html +54 -0
- taguette/templates/project.html +288 -0
- taguette/templates/project_delete.html +17 -0
- taguette/templates/project_import.html +122 -0
- taguette/templates/project_import_codebook.html +34 -0
- taguette/templates/project_import_codebook_confirm.html +49 -0
- taguette/templates/project_new.html +31 -0
- taguette/templates/tos.html +8 -0
- taguette/templates/trans.js +7 -0
- taguette/templates/welcome.html +15 -0
- taguette/utils.py +177 -0
- taguette/validate.py +141 -0
- taguette/version_check.py +102 -0
- taguette/web/__init__.py +252 -0
- taguette/web/api.py +1835 -0
- taguette/web/base.py +864 -0
- taguette/web/export.py +338 -0
- 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()
|