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,146 @@
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
+ import nextcloud
8
+ import os
9
+
10
+ log = logging.getLogger(__name__)
11
+
12
+
13
+ class Nextcloud:
14
+ def __init__(
15
+ self,
16
+ url=None,
17
+ username=None,
18
+ password=None,
19
+ folder=None,
20
+ **kwargs,
21
+ ):
22
+ self._nxc = None
23
+ self._root = None
24
+ self.url = url
25
+ self.username = username
26
+ self.password = password
27
+ self.root_dir = folder
28
+
29
+ def test_connection(self):
30
+ if all([self.url, self.username, self.password]):
31
+ return str(self.nxc.list_folders().status_code).startswith("2")
32
+ return False
33
+
34
+ @property
35
+ def nxc(self):
36
+ if self._nxc is None:
37
+ self._nxc = nextcloud.NextCloud(
38
+ endpoint=self.url,
39
+ user=self.username,
40
+ password=self.password,
41
+ )
42
+ return self._nxc
43
+
44
+ @property
45
+ def root(self):
46
+ if self._root is None:
47
+ log.debug("start get_folder")
48
+ self._root = self.nxc.get_folder(self.root_dir)
49
+ if self._root is None:
50
+ raise "Could not find the Root Directory"
51
+ return self._root
52
+
53
+ def nxc_list_files_and_folders(self, get_selectable=False):
54
+
55
+ selectable_folders = []
56
+
57
+ def recursive_list(item, depth=0):
58
+ items = []
59
+ for d in item.list():
60
+ if d.isdir():
61
+ children = recursive_list(d, depth=depth + 1)
62
+ contains_xml = any(
63
+ f.get("name", "").endswith(".xml") for f in children
64
+ )
65
+ item = {
66
+ "type": "folder",
67
+ "name": d.basename(),
68
+ "depth": depth,
69
+ "path": d.get_relative_path(),
70
+ "contains_xml": contains_xml,
71
+ "children": {
72
+ "count": len(children),
73
+ "list": children,
74
+ },
75
+ }
76
+ items.append(item)
77
+ if contains_xml:
78
+ selectable_folders.append(item)
79
+ else:
80
+ items.append(
81
+ {
82
+ "type": "file",
83
+ "name": d.basename(),
84
+ "depth": depth,
85
+ }
86
+ )
87
+ return items
88
+
89
+ result = recursive_list(self.root)
90
+ if get_selectable:
91
+ return selectable_folders
92
+ else:
93
+ return result
94
+
95
+ def get_selectable_folders(self):
96
+
97
+ return self.nxc_list_files_and_folders(get_selectable=True)
98
+
99
+ def download_nxc_files(self, file_paths, projectname):
100
+ for path in file_paths:
101
+ # Retrieve element (file or folder) from the server
102
+ try:
103
+ element = self.nxc.get_folder(path)
104
+ log.debug(f"Processing {path} in project {projectname}")
105
+
106
+ # Determine destination folder
107
+ destination_folder = (
108
+ f"projects/{projectname}/data/{element.basename()}"
109
+ )
110
+ log.debug(f"Destination folder: {destination_folder}")
111
+
112
+ if not os.path.exists(destination_folder):
113
+ os.makedirs(destination_folder)
114
+
115
+ # Recursive function to download files and folders
116
+ def download_recursive(
117
+ current_element, current_path, current_dest
118
+ ):
119
+ for item in current_element.list():
120
+ item_path = os.path.join(current_path, item.basename())
121
+ if item.isdir():
122
+ # If it's a folder, create subfolder and continue
123
+ # recursively
124
+ new_dest = os.path.join(
125
+ current_dest, item.basename()
126
+ )
127
+ if not os.path.exists(new_dest):
128
+ os.makedirs(new_dest)
129
+ subfolder = self.nxc.get_folder(item_path)
130
+ download_recursive(subfolder, item_path, new_dest)
131
+ else:
132
+ # If it's a file, download it
133
+ remote_file = self.nxc.get_file(item_path)
134
+ target_path = os.path.join(
135
+ current_dest, item.basename()
136
+ )
137
+ remote_file.download(
138
+ target=target_path, overwrite=True
139
+ )
140
+ log.debug(f"Downloaded: {target_path}")
141
+
142
+ # Start the recursive download
143
+ download_recursive(element, path, destination_folder)
144
+
145
+ except Exception as e:
146
+ log.error(f"Error processing {path}: {str(e)}")
tgp_backend/project.py ADDED
@@ -0,0 +1,468 @@
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
+ import json
8
+ import os
9
+ import subprocess
10
+
11
+ from datetime import datetime
12
+
13
+ from tg_model.collection import CollectionModeler # type: ignore
14
+ from tg_model.project import Project as ProjectHandler # type: ignore
15
+ from tg_model.tei import TEIParser # type: ignore
16
+
17
+ from tg_model.yaml import ( # type: ignore
18
+ CollectionConfig,
19
+ CollectionConfigTemplate,
20
+ ProjectConfigTemplate,
21
+ ProjectConfig,
22
+ )
23
+
24
+ from tgp_backend.config import MAIN_PATH
25
+ from tgp_backend.tgclient import TGclient
26
+ from tgp_backend.util import (
27
+ list_files_and_folders,
28
+ get_selectable_folders,
29
+ )
30
+
31
+ log = logging.getLogger(__name__)
32
+
33
+
34
+ class Project(object):
35
+ def __init__(self, path, username):
36
+ self.path = path
37
+ self.username = username
38
+ self._project_config = None
39
+ self._collections = None
40
+ self._nextcloud = None
41
+
42
+ @property
43
+ def name(self):
44
+ log.warning("Project.name is deprecated, use Project.path instead")
45
+ return self.path
46
+
47
+ @property
48
+ def fullpath(self):
49
+ return os.path.join(MAIN_PATH, self.username, self.path)
50
+ # return f"{MAIN_PATH}/{self.path}"
51
+
52
+ @property
53
+ def title(self):
54
+ return self.project_config.title
55
+
56
+ @property
57
+ def description(self):
58
+ return self.project_config.description
59
+
60
+ @property
61
+ def collectors(self):
62
+ return self.project_config.collectors
63
+
64
+ @property
65
+ def path_data(self):
66
+ return os.path.join(self.fullpath, "data")
67
+
68
+ @property
69
+ def path_metadata(self):
70
+ return os.path.join(self.fullpath, "metadata")
71
+
72
+ @property
73
+ def path_otherfiles(self):
74
+ return os.path.join(self.path_metadata, "other_files")
75
+
76
+ def create(self):
77
+ if not os.path.exists(self.fullpath):
78
+ os.makedirs(self.fullpath)
79
+ os.makedirs(self.path_data)
80
+ os.makedirs(self.path_metadata)
81
+ else:
82
+ log.warning("Project already exists!")
83
+
84
+ @property
85
+ def avatar(self):
86
+ if self.project_config.avatar:
87
+ return os.path.join(
88
+ self.path_otherfiles, self.project_config.avatar
89
+ )
90
+
91
+ @property
92
+ def xslt(self):
93
+ if self.project_config.xslt:
94
+ return os.path.join(self.path_otherfiles, self.project_config.xslt)
95
+
96
+ def save_avatar(self, file):
97
+ if file.filename and file.filename != "":
98
+ file.save(os.path.join(self.path_otherfiles, file.filename))
99
+ self.project_config.avatar = file.filename
100
+
101
+ def save_xslt(self, file):
102
+ if file.filename and file.filename != "":
103
+ file.save(os.path.join(self.path_otherfiles, file.filename))
104
+ self.project_config.xslt = file.filename
105
+
106
+ def clear_xslt(self):
107
+ self.project_config.xslt = None
108
+ self.project_config.save()
109
+
110
+ def update(self, title, description, collectors, avatar, xslt):
111
+ self.project_config.title = title
112
+ self.project_config.description = description
113
+ self.project_config.collectors = collectors
114
+
115
+ if avatar:
116
+ self.save_avatar(avatar)
117
+ self.save_xslt(xslt)
118
+
119
+ self.project_config.save()
120
+
121
+ def clone_git_project(self, url):
122
+ repo_name = url.split("/")[-1].replace(".git", "")
123
+ repo_path = os.path.join(self.path_data, repo_name)
124
+
125
+ if not os.path.exists(repo_path):
126
+ subprocess.run(["git", "clone", url, repo_path])
127
+ else:
128
+ log.info("Repository already exists!")
129
+
130
+ def file_upload(self, files):
131
+ for file in files:
132
+ if file:
133
+ filename = file.filename
134
+ filepath = os.path.join(self.path_data, filename)
135
+ directory = os.path.dirname(filepath)
136
+ if not os.path.exists(directory):
137
+ os.makedirs(directory)
138
+ file.save(filepath)
139
+
140
+ def list_files_and_folders(self):
141
+ return list_files_and_folders(self.path_data)
142
+
143
+ def get_selectable_folders(self):
144
+ selectable_folders = get_selectable_folders(self.path_data)
145
+ for folder in selectable_folders:
146
+ folder["relative_path"] = folder["path"].replace(
147
+ self.path_data, ""
148
+ )
149
+ return selectable_folders
150
+
151
+ @property
152
+ def main_config(self):
153
+ log.warning(
154
+ "Project.main_config is deprecated, use Project.project_config instead"
155
+ )
156
+ return self.project_config
157
+
158
+ @property
159
+ def project_config(self):
160
+ if not self._project_config:
161
+ self._project_config = ProjectConfig(
162
+ projectpath=self.path_metadata
163
+ )
164
+ if not self._project_config.exists():
165
+ self._project_config = ProjectConfigTemplate(
166
+ self.path_metadata
167
+ ).render()
168
+ self._project_config.title = self.path
169
+ self._project_config.save()
170
+
171
+ return self._project_config
172
+
173
+ @property
174
+ def project_handler(self):
175
+ return ProjectHandler(self.path_metadata)
176
+
177
+ def get_subproject_inpath(self, name):
178
+ for subproject in self.project_config.content["subprojects"]:
179
+ if subproject["name"] == name:
180
+ return subproject["inpath"]
181
+ return None
182
+
183
+ def _set_collections(self):
184
+ self._collections = {}
185
+ if self.project_config.exists():
186
+ for subproject in self.project_config.content["subprojects"]:
187
+ collection_config = CollectionConfig(subproject["basepath"])
188
+ if not collection_config.exists():
189
+ collection_config = CollectionConfigTemplate(
190
+ projectpath=self.path_metadata,
191
+ subproject=subproject,
192
+ files=[
193
+ TEIParser(fullpath=file)
194
+ for file in subproject["files"]
195
+ ],
196
+ ).render(overwrite=False)
197
+
198
+ c_modeler = CollectionModeler(subproject, self.path_metadata)
199
+ self._collections[subproject["name"]] = {
200
+ "config": collection_config,
201
+ "paths": subproject,
202
+ "parser": CollectionParser(collection_config),
203
+ "modeler": c_modeler,
204
+ "path": c_modeler.get_collection_path(),
205
+ }
206
+
207
+ @property
208
+ def collections(self):
209
+ if self._collections is None:
210
+ self._set_collections()
211
+ return self._collections
212
+
213
+ def get_collection_paths(self):
214
+ return [c["path"] for c in self.collections.values()]
215
+
216
+ def get_collection(self, title):
217
+ return self.collections.get(title)
218
+
219
+ def get_collection_parser(self, collection):
220
+ return CollectionParser(collection)
221
+
222
+ def get_tgp(self, instance):
223
+ return TGProject(self, instance)
224
+
225
+ def _save_validation_results(self, results, filepath):
226
+ """
227
+ Saves the validation results to a JSON file.
228
+ Adds the date of the last execution.
229
+ """
230
+ results["last_run"] = datetime.now().strftime("%Y-%m-%d %H:%M")
231
+
232
+ with open(filepath, "w", encoding="utf-8") as f:
233
+ json.dump(results, f, indent=4, ensure_ascii=False)
234
+
235
+ log.info(f"Validation results saved to {filepath}")
236
+
237
+ def _run_validation(self, file_attributes=False):
238
+ validation_results = self.project_handler.validate(
239
+ file_attributes=file_attributes
240
+ )
241
+ self._save_validation_results(validation_results, self.validation_file)
242
+ return validation_results
243
+
244
+ @property
245
+ def validation_file(self):
246
+ """
247
+ Returns the path to the validation results file.
248
+ """
249
+ return os.path.join(self.path_metadata, "validation_results.json")
250
+
251
+ def get_validation_results(self, refresh=False, file_attributes=False):
252
+ """
253
+ Returns stored validation results.
254
+ If no results are saved or `refresh=True`, a new validation will
255
+ be performed.
256
+ """
257
+ # If `refresh` is set or file doesn't exist, perform new validation
258
+ if refresh or not os.path.exists(self.validation_file):
259
+ validation_results = self._run_validation(
260
+ file_attributes=file_attributes
261
+ )
262
+ else:
263
+ # Load results from file
264
+ with open(self.validation_file, "r", encoding="utf-8") as f:
265
+ validation_results = json.load(f)
266
+
267
+ if "ready_for_publication" not in validation_results:
268
+ validation_results = self._run_validation(
269
+ file_attributes=file_attributes
270
+ )
271
+ return validation_results
272
+
273
+
274
+ class TGProject(object):
275
+
276
+ def __init__(self, project, instance, username=None):
277
+ if isinstance(project, Project):
278
+ self.project = project
279
+ elif username:
280
+ self.project = Project(project, username)
281
+ else:
282
+ raise ValueError(
283
+ "Project must be an instance of Project or a project path \
284
+ with a username."
285
+ )
286
+ self.instance = instance
287
+ self.project_config = self.project.project_config
288
+ self.collections = self.project.collections
289
+ self._tg_client = None
290
+ self._tg_session_id = None
291
+ self._tg_project_id = None
292
+
293
+ def check_session(self):
294
+ return self.tg_client.check_session()
295
+
296
+ @property
297
+ def tg_session_id(self):
298
+ if not self._tg_session_id:
299
+ self._tg_session_id = self.project_config.get_tg_session_id(
300
+ self.instance
301
+ )
302
+ return self._tg_session_id
303
+
304
+ @tg_session_id.setter
305
+ def tg_session_id(self, session_id):
306
+ self._tg_session_id = session_id
307
+ self.project_config.set_tg_session_id(session_id, self.instance)
308
+
309
+ @property
310
+ def imex_file(self):
311
+ return os.path.join(self.project_config.projectpath, "config.imex")
312
+
313
+ @property
314
+ def tg_project_id(self):
315
+ if not self._tg_project_id:
316
+ self._tg_project_id = self.project_config.get_tg_project_id(
317
+ self.instance
318
+ )
319
+ return self._tg_project_id
320
+
321
+ @tg_project_id.setter
322
+ def tg_project_id(self, project_id):
323
+ self._tg_project_id = project_id
324
+ self.project_config.set_tg_project_id(project_id, self.instance)
325
+
326
+ @property
327
+ def tg_client(self):
328
+ if not self._tg_client:
329
+ self._tg_client = TGclient(self.tg_session_id, self.instance)
330
+ return self._tg_client
331
+
332
+ def create_tg_project(self, name, description=""):
333
+ # def create_tg_project(self, name, instance="test", description=""):
334
+ if not self.tg_session_id:
335
+ return []
336
+ tg_project_id = self.tg_client.create_project(name, description)
337
+ if tg_project_id:
338
+ self.tg_project_id = tg_project_id
339
+
340
+ def clear_tg_project(self, tg_project_id):
341
+ # def clear_tg_project(self, tg_project_id, instance="test"):
342
+ if not self.tg_session_id:
343
+ return False
344
+ # clear contents of tg-project at textgrid server
345
+ self.tg_client.clear_project(tg_project_id)
346
+
347
+ # remove imex files locally
348
+ for c in self.project.collections.values():
349
+ c["modeler"].remove_imex_file()
350
+ self.project_config.other_files.remove_imex_file()
351
+
352
+ return True
353
+
354
+ def delete_tg_project(self, tg_project_id):
355
+ # def delete_tg_project(self, tg_project_id, instance="test"):
356
+ if not self.tg_session_id:
357
+ return False
358
+ # delete tg-project at textgrid server
359
+ res = self.tg_client.delete_project(tg_project_id)
360
+ # delete tg-project at local config if successful AND
361
+ # is the currently defined project_id
362
+ if res and tg_project_id == self.tg_project_id:
363
+ self.tg_project_id = None
364
+ return True
365
+
366
+ def get_tg_projects(self):
367
+ # def get_tg_projects(self, instance="test"):
368
+ if not self.tg_session_id:
369
+ return []
370
+ return self.tg_client.get_assigned_projects()
371
+
372
+ def get_tg_project(self, project_id):
373
+ if self.tg_session_id:
374
+ return self.tg_client.get_project_description(project_id)
375
+
376
+ def upload_tg_project(self):
377
+ # def upload_tg_project(self, instance="test"):
378
+ if not self.tg_session_id:
379
+ return False
380
+
381
+ for c_name in self.collections:
382
+ collection = self.collections[c_name]
383
+ # step 1: create required metadata
384
+ collection["modeler"].render_collection()
385
+ # step 2: upload collection to textgrid server
386
+ collection["modeler"].upload(
387
+ tg_session_id=self.tg_session_id,
388
+ tg_project_id=self.tg_project_id,
389
+ tg_server=self.instance,
390
+ )
391
+
392
+ def upload_collection(self, collection_name):
393
+ # def upload_collection(self, collection_name, instance="test"):
394
+ if not self.tg_session_id:
395
+ return False
396
+ log.info("Uploading collection (%s)..." % collection_name)
397
+ collection = self.project.get_collection(collection_name)
398
+ # step 1: create required metadata
399
+ collection["modeler"].render_collection()
400
+ # step 2: upload collection to textgrid server
401
+ collection["modeler"].upload(
402
+ tg_session_id=self.tg_session_id,
403
+ tg_project_id=self.tg_project_id,
404
+ tg_server=self.instance,
405
+ )
406
+
407
+ def upload_other_files(self):
408
+ # def upload_other_files(self, instance="test"):
409
+ if not self.tg_session_id:
410
+ return False
411
+ log.info("Uploading other files...")
412
+ self.project_config.other_files.upload(
413
+ tg_session_id=self.tg_session_id,
414
+ tg_project_id=self.tg_project_id,
415
+ tg_server=self.instance,
416
+ )
417
+
418
+ def get_project_contents(self):
419
+ return self.tg_client.get_project_contents(self.tg_project_id)
420
+
421
+ def publish_project(self):
422
+ # def publish_project(self, instance="test"):
423
+ if not self.tg_session_id:
424
+ return False
425
+ log.info("Publishing project...")
426
+ self.tg_client.publish(self.tg_project_id)
427
+
428
+
429
+ class CollectionParser(object):
430
+
431
+ def __init__(self, collection):
432
+ self.collection = collection
433
+ self._elements = None
434
+
435
+ @property
436
+ def elements(self):
437
+ if self._elements is None:
438
+ self._elements = []
439
+ for file in self.collection.elements:
440
+ self._elements.append(
441
+ {
442
+ "file": file,
443
+ "tei_parser": TEIParser(fullpath=file["fullpath"]),
444
+ }
445
+ )
446
+ return self._elements
447
+
448
+ def test_xpath(self, xpath):
449
+ results = []
450
+ for element in self.elements:
451
+ result = element["tei_parser"].find(xpath)
452
+ if result is not None:
453
+ results.append(
454
+ {
455
+ "filename": element["file"]["filename"],
456
+ "result": result,
457
+ }
458
+ )
459
+ return {
460
+ "results": results,
461
+ "count": {
462
+ "total": len(self.collection.elements),
463
+ "found": len(results),
464
+ "percentage": round(
465
+ len(results) / len(self.collection.elements) * 100
466
+ ),
467
+ },
468
+ }
@@ -0,0 +1,47 @@
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
+ log = logging.getLogger(__name__)
9
+
10
+
11
+ class SessionManager:
12
+ def __init__(self, session):
13
+ self.session = session
14
+
15
+
16
+ class SessionManagerNextcloud(SessionManager):
17
+
18
+ def save_credentials(self, form_data):
19
+ log.info("Saving credentials to session")
20
+ self.session["nextcloud"] = {
21
+ "username": form_data.get("nextcloud_user"),
22
+ "password": form_data.get("nextcloud_password"),
23
+ "url": form_data.get("nextcloud_url"),
24
+ "folder": form_data.get("nextcloud_folder", ""),
25
+ }
26
+
27
+ def get_credentials(self):
28
+ log.info("Retrieving credentials from session")
29
+ return self.session.get("nextcloud", {})
30
+
31
+ def delete_nextcloud_credentials(self):
32
+ log.info("Deleting credentials from session")
33
+ self.session.pop("nextcloud", None)
34
+
35
+ def request_has_valid_credentials(self, request):
36
+ """
37
+ Check if the request contains valid Nextcloud credentials.
38
+ """
39
+ relevant_form_keys = [
40
+ "nextcloud_url",
41
+ "nextcloud_user",
42
+ "nextcloud_password",
43
+ ]
44
+ relevant_form_values = [
45
+ request.form.get(key) for key in relevant_form_keys
46
+ ]
47
+ return all(relevant_form_values)