tg-prepare 2.2.2__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 (82) hide show
  1. tg_prepare-2.2.2.dist-info/METADATA +20 -0
  2. tg_prepare-2.2.2.dist-info/RECORD +82 -0
  3. tg_prepare-2.2.2.dist-info/WHEEL +5 -0
  4. tg_prepare-2.2.2.dist-info/entry_points.txt +4 -0
  5. tg_prepare-2.2.2.dist-info/licenses/LICENSE +202 -0
  6. tg_prepare-2.2.2.dist-info/projects/.secret_key +1 -0
  7. tg_prepare-2.2.2.dist-info/top_level.txt +2 -0
  8. tgp_backend/__init__.py +17 -0
  9. tgp_backend/auth.py +71 -0
  10. tgp_backend/cli.py +95 -0
  11. tgp_backend/config.py +35 -0
  12. tgp_backend/directories.py +79 -0
  13. tgp_backend/interfaces.py +3 -0
  14. tgp_backend/nextcloud.py +146 -0
  15. tgp_backend/project.py +468 -0
  16. tgp_backend/session_manager.py +47 -0
  17. tgp_backend/tgclient.py +187 -0
  18. tgp_backend/user.py +137 -0
  19. tgp_backend/util.py +131 -0
  20. tgp_ui/__init__.py +0 -0
  21. tgp_ui/app.py +150 -0
  22. tgp_ui/routes/__init__.py +0 -0
  23. tgp_ui/routes/auth.py +72 -0
  24. tgp_ui/routes/collection.py +319 -0
  25. tgp_ui/routes/data.py +229 -0
  26. tgp_ui/routes/project.py +103 -0
  27. tgp_ui/routes/publication.py +229 -0
  28. tgp_ui/routes/tabs.py +34 -0
  29. tgp_ui/routes/views.py +66 -0
  30. tgp_ui/static/css/bootstrap-icons.min.css +5 -0
  31. tgp_ui/static/css/bootstrap.min.css +6 -0
  32. tgp_ui/static/css/bootstrap.min.css.map +1 -0
  33. tgp_ui/static/css/main.css +141 -0
  34. tgp_ui/static/css/navbar.css +92 -0
  35. tgp_ui/static/css/simpleXML.css +67 -0
  36. tgp_ui/static/img/favicon.ico +0 -0
  37. tgp_ui/static/img/textgrid-logo.svg +1 -0
  38. tgp_ui/static/js/bootstrap.bundle.min.js +7 -0
  39. tgp_ui/static/js/collectionManager.js +60 -0
  40. tgp_ui/static/js/fileManager.js +153 -0
  41. tgp_ui/static/js/jquery.min.js +2 -0
  42. tgp_ui/static/js/main.js +36 -0
  43. tgp_ui/static/js/modalManager.js +105 -0
  44. tgp_ui/static/js/navbarManager.js +151 -0
  45. tgp_ui/static/js/projectManager.js +60 -0
  46. tgp_ui/static/js/require.js +5 -0
  47. tgp_ui/static/js/sidebarManager.js +44 -0
  48. tgp_ui/static/js/simpleXML.js +193 -0
  49. tgp_ui/static/js/tabManager.js +83 -0
  50. tgp_ui/templates/auth/login.html +42 -0
  51. tgp_ui/templates/details/empty_container.html +16 -0
  52. tgp_ui/templates/details/manage_collection.html +209 -0
  53. tgp_ui/templates/file_tree.html +39 -0
  54. tgp_ui/templates/includes/get_sessionid.html +29 -0
  55. tgp_ui/templates/includes/publish_form.html +55 -0
  56. tgp_ui/templates/includes/set_sessionid.html +26 -0
  57. tgp_ui/templates/includes/upload_form.html +258 -0
  58. tgp_ui/templates/layout.html +74 -0
  59. tgp_ui/templates/macros.html +101 -0
  60. tgp_ui/templates/modal/delete_project.html +25 -0
  61. tgp_ui/templates/modal/empty_container.html +9 -0
  62. tgp_ui/templates/modal/file_explorer_content.html +34 -0
  63. tgp_ui/templates/modal/file_explorer_main.html +22 -0
  64. tgp_ui/templates/modal/file_explorer_nextcloud.html +27 -0
  65. tgp_ui/templates/modal/github_modal.html +29 -0
  66. tgp_ui/templates/modal/nextcloud_login.html +48 -0
  67. tgp_ui/templates/modal/tei_explorer.html +58 -0
  68. tgp_ui/templates/modal/xpath_parser.html +52 -0
  69. tgp_ui/templates/nxc_login.html +25 -0
  70. tgp_ui/templates/nxc_tab.html +15 -0
  71. tgp_ui/templates/project_main.html +36 -0
  72. tgp_ui/templates/project_navbar.html +81 -0
  73. tgp_ui/templates/projects_main.html +58 -0
  74. tgp_ui/templates/tabs/check_upload.html +87 -0
  75. tgp_ui/templates/tabs/edit_project.html +113 -0
  76. tgp_ui/templates/tabs/empty_container.html +12 -0
  77. tgp_ui/templates/tabs/import_data.html +122 -0
  78. tgp_ui/templates/tabs/manage_collections.html +107 -0
  79. tgp_ui/templates/tabs/publication.html +42 -0
  80. tgp_ui/templates/tabs/select_directories.html +68 -0
  81. tgp_ui/templates/tabs/upload.html +42 -0
  82. tgp_ui/templates/tabs/validate_metadata.html +227 -0
@@ -0,0 +1,187 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright (C) 2023-2025 TU-Dresden (ZIH)
3
+ # ralf.klammer@tu-dresden.de
4
+ # moritz.wilhelm@tu-dresden.de
5
+ import logging
6
+
7
+ from tgadmin.tgadmin import _crud_delete_op
8
+ from tgadmin.tgimport import TGimport
9
+
10
+ from tgclients import (
11
+ TextgridAuth,
12
+ TextgridConfig,
13
+ TextgridCrud,
14
+ TextgridSearch,
15
+ )
16
+ from tgclients.config import PROD_SERVER, TEST_SERVER
17
+
18
+ log = logging.getLogger(__name__)
19
+
20
+
21
+ class TGclient(object):
22
+ def __init__(self, sid, instance="test", verbose=False):
23
+ self.sid = sid
24
+
25
+ if instance == "live":
26
+ self.server = PROD_SERVER
27
+ else:
28
+ self.server = TEST_SERVER
29
+ self.config = TextgridConfig(self.server)
30
+
31
+ self.crud = TextgridCrud(self.config)
32
+ self.tgauth = TextgridAuth(self.config)
33
+ self.tgsearch = TextgridSearch(self.config, nonpublic=True)
34
+
35
+ self._contents = {}
36
+
37
+ def check_session(self):
38
+ try:
39
+ if self.sid:
40
+ self.tgsearch.search(sid=self.sid)
41
+ return True
42
+ except Exception as e:
43
+ log.error(f"Error checking session id: {self.sid} : {e}")
44
+ return False
45
+ # return self.tgauth.check_session(self.sid) # returns username
46
+
47
+ def create_project(self, name, description=""):
48
+ log.info(f"Creating project {name}")
49
+ return self.tgauth.create_project(self.sid, name, description)
50
+
51
+ def clear_project(self, project_id):
52
+ # delete content of project
53
+ # repeat until whole content has been deleted
54
+ # (necessary because of default limit in '_crud_delete_op')
55
+ log.info(f"Clearing project contents {project_id}")
56
+ content = self.get_project_content(project_id)
57
+ handled_count = 0
58
+ initial_count = int(content.hits)
59
+ while handled_count < initial_count:
60
+ for tgobj in content.result:
61
+ # can only delete objects that are not already published
62
+ if tgobj.object_value.generic.generated.availability == None:
63
+ _crud_delete_op(self, tgobj)
64
+ handled_count += 1
65
+ content = self.get_project_content(project_id)
66
+
67
+ def delete_project(self, project_id):
68
+ self.clear_project(project_id)
69
+
70
+ log.info(f"Deleting project {project_id}")
71
+ return self.tgauth.delete_project(self.sid, project_id)
72
+
73
+ def get_project_content(self, project_id):
74
+ log.warning("Deprecated: use TGclient.get_project_contents() instead")
75
+ contents = self.tgsearch.search(
76
+ filters=["project.id:" + project_id], sid=self.sid
77
+ )
78
+ return contents
79
+
80
+ def _query_contents(self, tg_project_id):
81
+ if not self._contents.get(tg_project_id):
82
+ log.debug(f"Querying contents for project id: {tg_project_id}")
83
+ try:
84
+ self._contents[tg_project_id] = self.tgsearch.search(
85
+ filters=["project.id:" + tg_project_id],
86
+ sid=self.sid,
87
+ )
88
+ except Exception as e:
89
+ log.error(f"Error querying contents for project: {e}")
90
+ return []
91
+
92
+ return self._contents[tg_project_id].result
93
+
94
+ def get_project_contents(
95
+ self,
96
+ tg_project_id,
97
+ public=None,
98
+ count=False,
99
+ category=None,
100
+ ):
101
+ _filter = None
102
+ if category == "tei":
103
+ _filter = ["text/xml"]
104
+ elif category == "images":
105
+ _filter = ["image/jpeg", "image/png", "image/gif"]
106
+ elif category == "other":
107
+ _filter = [
108
+ "image/png",
109
+ "text/markdown",
110
+ "application/xslt+xml",
111
+ "text/tg.portalconfig+xml",
112
+ ]
113
+
114
+ contents = self._query_contents(tg_project_id)
115
+ results = {}
116
+ for tgobj in contents:
117
+ result = {}
118
+ result["title"] = tgobj.object_value.generic.provided.title[0]
119
+ result["tguri"] = (
120
+ tgobj.object_value.generic.generated.textgrid_uri.value
121
+ )
122
+ result["mime"] = tgobj.object_value.generic.provided.format
123
+ result["published"] = (
124
+ tgobj.object_value.generic.generated.availability is not None
125
+ )
126
+ log.debug(f"Found object: {result}")
127
+
128
+ if _filter is None or result["mime"] in _filter:
129
+ results[result["title"]] = result
130
+ else:
131
+ log.info(
132
+ "Skipping object with unsupported mime-type: %s"
133
+ % result["mime"]
134
+ )
135
+ return len(results) if count else results
136
+
137
+ def get_project_description(self, project_id):
138
+ desc = self.tgauth.get_project_description(project_id)
139
+ if desc:
140
+ return {
141
+ "id": project_id,
142
+ "name": desc.name,
143
+ "description": desc.description,
144
+ "tei_count": self.get_project_contents(
145
+ project_id, count=True, category="tei"
146
+ ),
147
+ "img_count": self.get_project_contents(
148
+ project_id, count=True, category="images"
149
+ ),
150
+ "other_count": self.get_project_contents(
151
+ project_id, count=True, category="other"
152
+ ),
153
+ "content_count": self.get_project_contents(
154
+ project_id, count=True
155
+ ),
156
+ }
157
+ else:
158
+ log.warning(f"Cannot find project description for: {project_id}")
159
+
160
+ def get_assigned_projects(self):
161
+ log.info("Listing assigned projects")
162
+ # TODO: this needs a better error handling to indicate user,
163
+ # that the session-id seeems to be invalid
164
+ try:
165
+ _projects = self.tgauth.list_assigned_projects(self.sid)
166
+ except Exception as e:
167
+ log.error(f"Error listing assigned projects: {e}")
168
+ return []
169
+
170
+ for project_id in _projects:
171
+ yield self.get_project_description(project_id)
172
+
173
+ # def upload(self, filenames: list[str], project_id: str, imex_file: str):
174
+ # tg_importer = TGimport(
175
+ # self.sid,
176
+ # self.crud,
177
+ # project_id=project_id,
178
+ # ignore_warnings=True,
179
+ # imex_location=imex_file,
180
+ # )
181
+ # tg_importer.upload(filenames=filenames, threaded=True)
182
+
183
+ def publish(self, tg_project_id):
184
+ log.info(f"Publishing project {tg_project_id}")
185
+ # TODO: implement publish logic in tgclients
186
+ # see tgadmin.tgadmin.publish() tgadmin.py::Z646
187
+ breakpoint()
tgp_backend/user.py ADDED
@@ -0,0 +1,137 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright (C) 2023-2025 TU-Dresden (ZIH)
3
+ # ralf.klammer@tu-dresden.de
4
+ # moritz.wilhelm@tu-dresden.de
5
+
6
+ import logging
7
+
8
+ import os
9
+ import json
10
+ import hashlib
11
+
12
+ from flask_login import UserMixin # type: ignore
13
+
14
+ from .config import MAIN_PATH
15
+
16
+ log = logging.getLogger(__name__)
17
+
18
+
19
+ class User(UserMixin):
20
+ def __init__(
21
+ self,
22
+ username,
23
+ password_hash=None,
24
+ email=None,
25
+ role="user",
26
+ is_active=True,
27
+ ):
28
+ self.id = username
29
+ self.username = username
30
+ self.password_hash = password_hash
31
+ self.email = email
32
+ self.role = role
33
+ self._is_active = is_active # Underscore prefix for internal attribute
34
+ self.last_login = None
35
+
36
+ @property
37
+ def is_active(self):
38
+ """Return True if the user is active."""
39
+ return self._is_active
40
+
41
+ @is_active.setter
42
+ def is_active(self, value):
43
+ """Set the active status of the user."""
44
+ self._is_active = value
45
+
46
+ def check_password(self, password):
47
+ """Verify password against stored hash"""
48
+ hashed = hashlib.sha256(password.encode()).hexdigest()
49
+ return hashed == self.password_hash
50
+
51
+ def to_dict(self):
52
+ """Converts User to dictionary for JSON storage"""
53
+ return {
54
+ "username": self.username,
55
+ "password_hash": self.password_hash,
56
+ "email": self.email,
57
+ "role": self.role,
58
+ "is_active": self._is_active, # Use internal attribute
59
+ "last_login": self.last_login,
60
+ }
61
+
62
+ @staticmethod
63
+ def from_dict(data):
64
+ """Creates User object from dictionary"""
65
+ user = User(
66
+ username=data["username"],
67
+ password_hash=data["password_hash"],
68
+ email=data.get("email"),
69
+ role=data.get("role", "user"),
70
+ is_active=data.get("is_active", True),
71
+ )
72
+ user.last_login = data.get("last_login")
73
+ return user
74
+
75
+
76
+ class UserManager:
77
+ def __init__(self):
78
+ self.users_file = os.path.join(MAIN_PATH, "users.json")
79
+ os.makedirs(os.path.dirname(self.users_file), exist_ok=True)
80
+ self.load_users()
81
+
82
+ def load_users(self):
83
+ """Loads users from JSON file"""
84
+ self.users = {}
85
+ if os.path.exists(self.users_file):
86
+ try:
87
+ with open(self.users_file, "r") as f:
88
+ users_data = json.load(f)
89
+ for username, data in users_data.items():
90
+ self.users[username] = User.from_dict(data)
91
+ self.initialize_user_folder(
92
+ username
93
+ ) # Initialisiere Benutzerordner
94
+ except Exception as e:
95
+ print(f"Error loading users: {e}")
96
+ self.create_default_admin()
97
+ else:
98
+ self.create_default_admin()
99
+
100
+ def save_users(self):
101
+ """Saves users to JSON file"""
102
+ users_data = {
103
+ username: user.to_dict() for username, user in self.users.items()
104
+ }
105
+ with open(self.users_file, "w") as f:
106
+ json.dump(users_data, f, indent=2)
107
+
108
+ def get_user(self, username):
109
+ """Returns user by username"""
110
+ return self.users.get(username)
111
+
112
+ def create_user(self, username, password, email=None, role="user"):
113
+ """Creates new user"""
114
+ if username in self.users:
115
+ return False, "Username already taken"
116
+
117
+ password_hash = hashlib.sha256(password.encode()).hexdigest()
118
+ user = User(username, password_hash, email, role)
119
+ self.users[username] = user
120
+ self.save_users()
121
+ self.initialize_user_folder(username) # Initialisiere Benutzerordner
122
+ return True, "User created successfully"
123
+
124
+ def create_default_admin(self):
125
+ """Creates default admin user"""
126
+ self.create_user("admin", "admin123", role="admin")
127
+ print("Default admin user created (admin/admin123)")
128
+
129
+ def initialize_user_folder(self, username):
130
+ """Initialisiert den Benutzer-spezifischen Ordner"""
131
+ user_folder = os.path.join(MAIN_PATH, username)
132
+ os.makedirs(user_folder, exist_ok=True)
133
+ log.info(f"Benutzerordner initialisiert: {user_folder}")
134
+
135
+ def get_user_folder(self, username):
136
+ """Gibt den Pfad des Benutzerordners zurück"""
137
+ return os.path.join(MAIN_PATH, username)
tgp_backend/util.py ADDED
@@ -0,0 +1,131 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright (C) 2023-2024 TU-Dresden (ZIH)
3
+ # ralf.klammer@tu-dresden.de
4
+ import logging
5
+
6
+ import os
7
+ import re
8
+
9
+ from collections import defaultdict
10
+
11
+ from .config import get_config_value
12
+
13
+ log = logging.getLogger(__name__)
14
+
15
+
16
+ def config(*args, **kwargs):
17
+ log.warning(
18
+ """
19
+ Using deprecated config function.
20
+ Please use tgp_backend.config.get_config_value instead.
21
+ """
22
+ )
23
+ return get_config_value(*args, **kwargs)
24
+
25
+
26
+ def cli_startup(log_level=logging.INFO, log_file=None):
27
+ log_config = dict(
28
+ level=log_level,
29
+ format="%(asctime)s %(name)-10s %(levelname)-4s %(message)s",
30
+ )
31
+ if log_file:
32
+ log_config["filename"] = log_file
33
+
34
+ logging.basicConfig(**log_config)
35
+ logging.getLogger("").setLevel(log_level)
36
+
37
+
38
+ def get_file_extension(fname):
39
+ found_extension = re.search("\.[A-Za-z0-9]*$", fname, re.IGNORECASE)
40
+ if found_extension:
41
+ return found_extension[0][1:].lower()
42
+ return ""
43
+
44
+
45
+ def list_files_and_folders(path, get_selectable=False):
46
+
47
+ selectable_folders = []
48
+
49
+ def recursive_list(dir_path, depth=0):
50
+ items = []
51
+ for entry in os.scandir(dir_path):
52
+ if entry.name.startswith("."):
53
+ continue
54
+
55
+ if entry.is_dir():
56
+ children = recursive_list(entry.path, depth=depth + 1)
57
+ contains_xml = any(
58
+ f.get("name", "").endswith(".xml") for f in children
59
+ )
60
+ item = {
61
+ "type": "folder",
62
+ "name": entry.name,
63
+ "depth": depth,
64
+ "path": entry.path,
65
+ "contains_xml": contains_xml,
66
+ "children": {"count": len(children), "list": children},
67
+ }
68
+ items.append(item)
69
+ if contains_xml:
70
+ selectable_folders.append(item)
71
+ else:
72
+ items.append(
73
+ {
74
+ "type": "file",
75
+ "name": entry.name,
76
+ "depth": depth,
77
+ "path": entry.path,
78
+ }
79
+ )
80
+ return items
81
+
82
+ result = recursive_list(path)
83
+ if get_selectable:
84
+ return selectable_folders
85
+ else:
86
+ return result
87
+
88
+
89
+ def get_selectable_folders(path):
90
+
91
+ return list_files_and_folders(path, get_selectable=True)
92
+
93
+
94
+ def remove_empty_strings_from_dict(d):
95
+ for key in d:
96
+ if d[key] == "":
97
+ d[key] = None
98
+ return d
99
+
100
+
101
+ def parse_request_data(request, attrib_prefix):
102
+ """
103
+ Processes form data and creates a list of dictionaries
104
+ for the specified attributes.
105
+ """
106
+ grouped_data = defaultdict(lambda: defaultdict(list))
107
+
108
+ # Group data based on prefix
109
+ for key in request.form.keys():
110
+ if key.startswith(attrib_prefix):
111
+ try:
112
+ _, field, data_type = key.split("-")
113
+ grouped_data[field][data_type] = request.form.getlist(key)
114
+ except ValueError:
115
+ log.warning(
116
+ f"Invalid key format: {key}. Expected format: '{attrib_prefix}-field-data_type'."
117
+ )
118
+ continue # Skip invalid keys
119
+
120
+ # Transform grouped data into desired structure
121
+ max_length = max(len(data["xpath"]) for data in grouped_data.values())
122
+ return [
123
+ {
124
+ field: {
125
+ "xpath": data["xpath"][i],
126
+ "value": data["value"][i],
127
+ }
128
+ for field, data in grouped_data.items()
129
+ }
130
+ for i in range(max_length)
131
+ ]
tgp_ui/__init__.py ADDED
File without changes
tgp_ui/app.py ADDED
@@ -0,0 +1,150 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright (C) 2023-2025 TU-Dresden (ZIH)
3
+ # ralf.klammer@tu-dresden.de
4
+ # moritz.wilhelm@tu-dresden.de
5
+
6
+ import logging
7
+
8
+ import click # type: ignore
9
+ import os
10
+
11
+ from flask import Flask, render_template, session # type: ignore
12
+ from flask_json import FlaskJSON # type: ignore
13
+ from flask_login import LoginManager, current_user # type: ignore
14
+
15
+ from tgp_backend.project import Project
16
+
17
+ from tgp_backend.auth import SecretKeyManager
18
+ from tgp_backend.config import LOG_LEVEL, MAIN_PATH, get_config_value
19
+ from tgp_backend.nextcloud import Nextcloud
20
+ from tgp_backend.user import UserManager
21
+
22
+ from .routes.project import project_routes
23
+ from .routes.views import main_views
24
+ from .routes.data import data_routes
25
+ from .routes.tabs import tab_manager
26
+ from .routes.collection import collection_routes
27
+ from .routes.publication import publication_routes
28
+ from .routes.auth import auth_routes
29
+
30
+ log = logging.getLogger(__name__)
31
+
32
+ log_level = get_config_value("log", "level", default="DEBUG") == "DEBUG"
33
+ log.setLevel(log_level)
34
+ file_handler = logging.FileHandler(
35
+ get_config_value("log", "path", default="/tmp/tg_prepare.log")
36
+ )
37
+ file_handler.setLevel(log_level)
38
+ logging.getLogger().addHandler(file_handler)
39
+
40
+ base_params = {
41
+ "title": "TG Prepare",
42
+ }
43
+
44
+ app = Flask(__name__)
45
+ FlaskJSON(app)
46
+
47
+ secret_manager = SecretKeyManager(MAIN_PATH)
48
+ app.secret_key = secret_manager.secret_key
49
+
50
+ # Additional security settings, if not in DEBUG mode
51
+ app.config.update(SESSION_COOKIE_NAME="tgp_session")
52
+ if LOG_LEVEL != "DEBUG":
53
+ app.config.update(
54
+ SESSION_COOKIE_SECURE=True, # Only allow cookies over HTTPS
55
+ SESSION_COOKIE_HTTPONLY=True, # Prevent JavaScript access to cookies
56
+ SESSION_COOKIE_SAMESITE="Strict", # Protect against CSRF attacks
57
+ )
58
+
59
+ # Initialize Authentication and User Management
60
+ login_manager = LoginManager()
61
+ login_manager.init_app(app)
62
+ login_manager.login_view = (
63
+ "auth.login" # Redirect to login page if not authenticated
64
+ )
65
+
66
+
67
+ # Initialize user loader for Flask-Login
68
+ @login_manager.user_loader
69
+ def load_user(user_id):
70
+ return user_manager.get_user(user_id)
71
+
72
+
73
+ # Initialize UserManager
74
+ user_manager = UserManager()
75
+ app.user_manager = user_manager
76
+
77
+
78
+ # Initialize Blueprints
79
+ app.register_blueprint(main_views)
80
+ app.register_blueprint(project_routes)
81
+ app.register_blueprint(data_routes)
82
+ app.register_blueprint(tab_manager)
83
+ app.register_blueprint(collection_routes)
84
+ app.register_blueprint(publication_routes)
85
+ app.register_blueprint(auth_routes)
86
+
87
+
88
+ def get_textgrid_login_url(instance):
89
+ """
90
+ Returns the TextGrid login URL based on the instance (test or production).
91
+ """
92
+ if instance == "test":
93
+ return "https://test.textgridlab.org/1.0/Shibboleth.sso/Login?target=/1.0/secure/TextGrid-WebAuth.php?authZinstance=test.textgridlab.org"
94
+ else:
95
+ return "https://textgridlab.org/1.0/Shibboleth.sso/Login?target=/1.0/secure/TextGrid-WebAuth.php?authZinstance=textgrid-esx2.gwdg.de"
96
+
97
+
98
+ def get_projects():
99
+ projects = []
100
+ if current_user.is_authenticated:
101
+ path = os.path.join(MAIN_PATH, current_user.username)
102
+ for sub in os.listdir(path):
103
+ projectpath = os.path.join(path, sub)
104
+ if os.path.isdir(projectpath):
105
+ projects.append(Project(sub, current_user.username))
106
+ return projects
107
+
108
+
109
+ app.jinja_env.globals.update(
110
+ len=len,
111
+ round=round,
112
+ title="TG Prepare",
113
+ get_projects=get_projects,
114
+ get_textgrid_login_url=get_textgrid_login_url,
115
+ )
116
+
117
+
118
+ def _startup():
119
+
120
+ # Create the projects directory if it does not exist
121
+ if not os.path.exists(MAIN_PATH):
122
+ os.makedirs(MAIN_PATH)
123
+
124
+ logging.getLogger("zeep").setLevel(logging.INFO)
125
+ app.run(
126
+ host=get_config_value("main", "host", default="0.0.0.0"),
127
+ port=get_config_value("main", "port", default=8077),
128
+ debug=get_config_value("log", "level", default="DEBUG") == "DEBUG",
129
+ )
130
+
131
+
132
+ @click.command()
133
+ @click.option("--path", "-p", default=None)
134
+ def startup(path):
135
+ base_params["path"] = path if path else os.getcwd()
136
+ _startup()
137
+
138
+
139
+ @app.route("/nextcloud_tab/", methods=["POST"])
140
+ def nextcloud_tab():
141
+ nextcloud = Nextcloud(**session)
142
+ return render_template(
143
+ "nxc_tab.html",
144
+ nextcloud=nextcloud if nextcloud.test_connection() else None,
145
+ user=session.get("username", "-"),
146
+ )
147
+
148
+
149
+ if __name__ == "__main__":
150
+ startup()
File without changes
tgp_ui/routes/auth.py ADDED
@@ -0,0 +1,72 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright (C) 2023-2025 TU-Dresden (ZIH)
3
+ # ralf.klammer@tu-dresden.de
4
+ # moritz.wilhelm@tu-dresden.de
5
+
6
+ import logging
7
+
8
+ from datetime import datetime
9
+
10
+ from flask import ( # type: ignore
11
+ Blueprint,
12
+ render_template,
13
+ redirect,
14
+ url_for,
15
+ request,
16
+ flash,
17
+ current_app,
18
+ )
19
+ from flask_login import ( # type: ignore
20
+ login_user,
21
+ logout_user,
22
+ login_required,
23
+ current_user,
24
+ )
25
+
26
+ log = logging.getLogger(__name__)
27
+
28
+ # Create Blueprint for authentication routes
29
+ auth_routes = Blueprint("auth", __name__)
30
+
31
+
32
+ @auth_routes.route("/login", methods=["GET", "POST"])
33
+ def login():
34
+ # Redirect if user is already authenticated
35
+ if current_user.is_authenticated:
36
+ return redirect(url_for("views.overview"))
37
+
38
+ if request.method == "POST":
39
+ username = request.form.get("username")
40
+ password = request.form.get("password")
41
+ # Check if "remember me" option is selected
42
+ remember = True if request.form.get("remember") else False
43
+
44
+ user_manager = current_app.user_manager
45
+ user = user_manager.get_user(username)
46
+
47
+ # Validate user credentials
48
+ if not user or not user.check_password(password):
49
+ flash(
50
+ "Bitte überprüfe deine Anmeldedaten und versuche es erneut.",
51
+ "danger",
52
+ )
53
+ return render_template("auth/login.html")
54
+
55
+ # Login successful
56
+ user.last_login = datetime.now().isoformat()
57
+ user_manager.save_users()
58
+ login_user(user, remember=remember)
59
+
60
+ # Redirect to requested page or default to overview
61
+ next_page = request.args.get("next")
62
+ return redirect(next_page or url_for("views.overview"))
63
+
64
+ return render_template("auth/login.html")
65
+
66
+
67
+ @auth_routes.route("/logout")
68
+ @login_required
69
+ def logout():
70
+ # Log out the current user
71
+ logout_user()
72
+ return redirect(url_for("auth.login"))