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.
- tg_prepare-2.2.2.dist-info/METADATA +20 -0
- tg_prepare-2.2.2.dist-info/RECORD +82 -0
- tg_prepare-2.2.2.dist-info/WHEEL +5 -0
- tg_prepare-2.2.2.dist-info/entry_points.txt +4 -0
- tg_prepare-2.2.2.dist-info/licenses/LICENSE +202 -0
- tg_prepare-2.2.2.dist-info/projects/.secret_key +1 -0
- tg_prepare-2.2.2.dist-info/top_level.txt +2 -0
- tgp_backend/__init__.py +17 -0
- tgp_backend/auth.py +71 -0
- tgp_backend/cli.py +95 -0
- tgp_backend/config.py +35 -0
- tgp_backend/directories.py +79 -0
- tgp_backend/interfaces.py +3 -0
- tgp_backend/nextcloud.py +146 -0
- tgp_backend/project.py +468 -0
- tgp_backend/session_manager.py +47 -0
- tgp_backend/tgclient.py +187 -0
- tgp_backend/user.py +137 -0
- tgp_backend/util.py +131 -0
- tgp_ui/__init__.py +0 -0
- tgp_ui/app.py +150 -0
- tgp_ui/routes/__init__.py +0 -0
- tgp_ui/routes/auth.py +72 -0
- tgp_ui/routes/collection.py +319 -0
- tgp_ui/routes/data.py +229 -0
- tgp_ui/routes/project.py +103 -0
- tgp_ui/routes/publication.py +229 -0
- tgp_ui/routes/tabs.py +34 -0
- tgp_ui/routes/views.py +66 -0
- tgp_ui/static/css/bootstrap-icons.min.css +5 -0
- tgp_ui/static/css/bootstrap.min.css +6 -0
- tgp_ui/static/css/bootstrap.min.css.map +1 -0
- tgp_ui/static/css/main.css +141 -0
- tgp_ui/static/css/navbar.css +92 -0
- tgp_ui/static/css/simpleXML.css +67 -0
- tgp_ui/static/img/favicon.ico +0 -0
- tgp_ui/static/img/textgrid-logo.svg +1 -0
- tgp_ui/static/js/bootstrap.bundle.min.js +7 -0
- tgp_ui/static/js/collectionManager.js +60 -0
- tgp_ui/static/js/fileManager.js +153 -0
- tgp_ui/static/js/jquery.min.js +2 -0
- tgp_ui/static/js/main.js +36 -0
- tgp_ui/static/js/modalManager.js +105 -0
- tgp_ui/static/js/navbarManager.js +151 -0
- tgp_ui/static/js/projectManager.js +60 -0
- tgp_ui/static/js/require.js +5 -0
- tgp_ui/static/js/sidebarManager.js +44 -0
- tgp_ui/static/js/simpleXML.js +193 -0
- tgp_ui/static/js/tabManager.js +83 -0
- tgp_ui/templates/auth/login.html +42 -0
- tgp_ui/templates/details/empty_container.html +16 -0
- tgp_ui/templates/details/manage_collection.html +209 -0
- tgp_ui/templates/file_tree.html +39 -0
- tgp_ui/templates/includes/get_sessionid.html +29 -0
- tgp_ui/templates/includes/publish_form.html +55 -0
- tgp_ui/templates/includes/set_sessionid.html +26 -0
- tgp_ui/templates/includes/upload_form.html +258 -0
- tgp_ui/templates/layout.html +74 -0
- tgp_ui/templates/macros.html +101 -0
- tgp_ui/templates/modal/delete_project.html +25 -0
- tgp_ui/templates/modal/empty_container.html +9 -0
- tgp_ui/templates/modal/file_explorer_content.html +34 -0
- tgp_ui/templates/modal/file_explorer_main.html +22 -0
- tgp_ui/templates/modal/file_explorer_nextcloud.html +27 -0
- tgp_ui/templates/modal/github_modal.html +29 -0
- tgp_ui/templates/modal/nextcloud_login.html +48 -0
- tgp_ui/templates/modal/tei_explorer.html +58 -0
- tgp_ui/templates/modal/xpath_parser.html +52 -0
- tgp_ui/templates/nxc_login.html +25 -0
- tgp_ui/templates/nxc_tab.html +15 -0
- tgp_ui/templates/project_main.html +36 -0
- tgp_ui/templates/project_navbar.html +81 -0
- tgp_ui/templates/projects_main.html +58 -0
- tgp_ui/templates/tabs/check_upload.html +87 -0
- tgp_ui/templates/tabs/edit_project.html +113 -0
- tgp_ui/templates/tabs/empty_container.html +12 -0
- tgp_ui/templates/tabs/import_data.html +122 -0
- tgp_ui/templates/tabs/manage_collections.html +107 -0
- tgp_ui/templates/tabs/publication.html +42 -0
- tgp_ui/templates/tabs/select_directories.html +68 -0
- tgp_ui/templates/tabs/upload.html +42 -0
- tgp_ui/templates/tabs/validate_metadata.html +227 -0
tgp_backend/tgclient.py
ADDED
|
@@ -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"))
|