tg-prepare 1.1.0__py3-none-any.whl → 2.1.0b1__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.

Potentially problematic release.


This version of tg-prepare might be problematic. Click here for more details.

Files changed (46) hide show
  1. {tg_prepare-1.1.0.dist-info → tg_prepare-2.1.0b1.dist-info}/METADATA +3 -3
  2. tg_prepare-2.1.0b1.dist-info/RECORD +54 -0
  3. {tg_prepare-1.1.0.dist-info → tg_prepare-2.1.0b1.dist-info}/WHEEL +1 -1
  4. tg_prepare-2.1.0b1.dist-info/projects/.secret_key +1 -0
  5. tgp_backend/__init__.py +3 -4
  6. tgp_backend/config.py +31 -0
  7. tgp_backend/directories.py +6 -8
  8. tgp_backend/nextcloud.py +40 -19
  9. tgp_backend/project.py +172 -45
  10. tgp_backend/session_manager.py +47 -0
  11. tgp_backend/util.py +73 -25
  12. tgp_ui/app.py +43 -335
  13. tgp_ui/routes/__init__.py +0 -0
  14. tgp_ui/routes/collection.py +272 -0
  15. tgp_ui/routes/data.py +228 -0
  16. tgp_ui/routes/project.py +102 -0
  17. tgp_ui/routes/publication.py +129 -0
  18. tgp_ui/routes/tabs.py +34 -0
  19. tgp_ui/routes/views.py +62 -0
  20. tgp_ui/static/css/navbar.css +92 -0
  21. tgp_ui/static/js/collectionManager.js +60 -0
  22. tgp_ui/static/js/fileManager.js +186 -0
  23. tgp_ui/static/js/main.js +32 -485
  24. tgp_ui/static/js/modalManager.js +105 -0
  25. tgp_ui/static/js/navbarManager.js +151 -0
  26. tgp_ui/static/js/projectManager.js +60 -0
  27. tgp_ui/static/js/require.js +5 -0
  28. tgp_ui/static/js/sidebarManager.js +32 -0
  29. tgp_ui/static/js/tabManager.js +79 -0
  30. tgp_ui/templates/layout.html +9 -48
  31. tgp_ui/templates/macros.html +79 -72
  32. tgp_ui/templates/project_main.html +36 -0
  33. tgp_ui/templates/project_navbar.html +81 -0
  34. tgp_ui/templates/{projects.html → projects_main.html} +13 -28
  35. tgp_ui/templates/tab_final_upload.html +29 -0
  36. tg_prepare-1.1.0.dist-info/RECORD +0 -39
  37. tgp_ui/templates/collection.html +0 -194
  38. tgp_ui/templates/file_upload.html +0 -24
  39. tgp_ui/templates/nxc_file_tree.html +0 -33
  40. tgp_ui/templates/project.html +0 -26
  41. tgp_ui/templates/storage.html +0 -49
  42. tgp_ui/templates/tei_explorer.html +0 -48
  43. tgp_ui/templates/xpath_parser_modal_content.html +0 -37
  44. {tg_prepare-1.1.0.dist-info → tg_prepare-2.1.0b1.dist-info}/entry_points.txt +0 -0
  45. {tg_prepare-1.1.0.dist-info → tg_prepare-2.1.0b1.dist-info}/licenses/LICENSE +0 -0
  46. {tg_prepare-1.1.0.dist-info → tg_prepare-2.1.0b1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,272 @@
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
+
10
+ from flask import Blueprint, render_template, request, send_file
11
+ from flask_json import json_response
12
+
13
+ from io import BytesIO
14
+
15
+ from tg_model.tei import TEIParser
16
+
17
+ from tgp_backend.directories import generateList
18
+ from tgp_backend.project import Project
19
+ from tgp_backend.util import parse_request_data
20
+
21
+ log = logging.getLogger(__name__)
22
+
23
+ collection_routes = Blueprint("collection", __name__)
24
+
25
+
26
+ # ***TABS***
27
+ # **********
28
+ @collection_routes.route(
29
+ "/tab_select_directories/<string:projectname>", methods=["GET", "POST"]
30
+ )
31
+ def tab_select_directories(projectname):
32
+
33
+ project = Project(projectname)
34
+ if request.method == "POST":
35
+ project.project_config.update(
36
+ tei_directories=request.form.getlist("selected_folder")
37
+ )
38
+
39
+ tei_directories = [
40
+ sp["inpath"] for sp in project.project_config.get_subprojects()
41
+ ]
42
+
43
+ return render_template(
44
+ "tabs/select_directories.html",
45
+ project=project,
46
+ selected_directories=tei_directories,
47
+ )
48
+
49
+
50
+ @collection_routes.route(
51
+ "/tab_manage_collections/<string:projectname>", methods=["GET"]
52
+ )
53
+ def tab_manage_collections(projectname):
54
+
55
+ project = Project(projectname)
56
+
57
+ return render_template(
58
+ "tabs/manage_collections.html",
59
+ project=project,
60
+ )
61
+
62
+
63
+ @collection_routes.route(
64
+ "/tab_validate_metadata/<string:projectname>", methods=["GET"]
65
+ )
66
+ @collection_routes.route(
67
+ "/tab_validate_metadata/<string:projectname>/<string:refresh>",
68
+ methods=["GET"],
69
+ )
70
+ def tab_validate_metadata(projectname, refresh=False):
71
+
72
+ project = Project(projectname)
73
+ return render_template(
74
+ "tabs/validate_metadata.html",
75
+ projectname=projectname,
76
+ validation_results=project.get_validation_results(refresh=refresh),
77
+ )
78
+
79
+
80
+ # ***OTHER***
81
+ # **********
82
+ @collection_routes.route(
83
+ "/load_collection/<string:projectname>/<string:collectionname>",
84
+ methods=["GET"],
85
+ )
86
+ def load_collection(projectname, collectionname):
87
+ collection = None
88
+ project = Project(projectname)
89
+ collection = None
90
+ if collectionname:
91
+ collection = project.get_collection(collectionname)
92
+
93
+ return render_template(
94
+ "details/manage_collection.html",
95
+ collectionname=collectionname,
96
+ collection=collection["config"],
97
+ project=project,
98
+ )
99
+
100
+
101
+ @collection_routes.route(
102
+ "/save_collection_attributes/<string:projectname>/<string:collectionname>",
103
+ methods=["POST"],
104
+ )
105
+ def save_collection_attributes(projectname, collectionname):
106
+ project = Project(projectname)
107
+ collection = project.get_collection(collectionname)
108
+ collection_config = collection["config"]
109
+
110
+ multi_attribs = ["collector", "rights_holder"]
111
+ for multi_attrib in multi_attribs:
112
+ elements = []
113
+ for name, url in zip(
114
+ request.form.getlist(f"{multi_attrib}_name"),
115
+ request.form.getlist(f"{multi_attrib}_url"),
116
+ ):
117
+ elements.append({"name": name, "url": url})
118
+ setattr(collection_config, multi_attrib, elements)
119
+
120
+ for attrib in ["short_title", "long_title"]:
121
+ value = request.form.get(attrib)
122
+ setattr(collection_config, attrib, value)
123
+
124
+ collection_config.save()
125
+ return load_collection(
126
+ projectname=projectname, collectionname=collectionname
127
+ )
128
+
129
+
130
+ @collection_routes.route(
131
+ "/save_collection_classifications/<string:projectname>/<string:collectionname>",
132
+ methods=["POST"],
133
+ )
134
+ def save_collection_classifications(projectname, collectionname):
135
+ project = Project(projectname)
136
+ collection = project.get_collection(collectionname)
137
+ collection_config = collection["config"]
138
+
139
+ # Save basic classifications...
140
+ collection_config.basic_classifications = parse_request_data(
141
+ request, "basic_classifications"
142
+ )
143
+ # ...and GND subjects
144
+ collection_config.gnd_subjects = parse_request_data(
145
+ request, "gnd_subjects"
146
+ )
147
+
148
+ collection_config.save()
149
+ return load_collection(
150
+ projectname=projectname, collectionname=collectionname
151
+ )
152
+
153
+
154
+ @collection_routes.route(
155
+ "/save_collection_metadata/<string:projectname>/<string:collectionname>",
156
+ methods=["POST"],
157
+ )
158
+ def save_collection_metadata(projectname, collectionname):
159
+ project = Project(projectname)
160
+ collection = project.get_collection(collectionname)
161
+ collection_config = collection["config"]
162
+ for attrib in request.form:
163
+ if "xpath" in attrib:
164
+ key = attrib.replace("_xpath", "")
165
+ type_ = "xpath"
166
+ elif "value" in attrib:
167
+ key = attrib.replace("_value", "")
168
+ type_ = "value"
169
+ if key in collection_config.xpath_or_value_attribs:
170
+ cc_attrib = getattr(collection_config, key)
171
+ cc_attrib[type_] = request.form.get(attrib)
172
+ else:
173
+ log.warning(
174
+ f"Attribute '{attrib}' not found in collection config."
175
+ )
176
+ collection_config.save()
177
+ return load_collection(
178
+ projectname=projectname, collectionname=collectionname
179
+ )
180
+
181
+
182
+ @collection_routes.route(
183
+ "/modal/tei_explorer/<string:projectname>/<string:collectionname>",
184
+ methods=["GET", "POST"],
185
+ )
186
+ def modal_tei_explorer(projectname, collectionname):
187
+ project = Project(projectname)
188
+ # collection = project.get_collection(collectionname)
189
+ dir_list_dict, file_list_dict = generateList(
190
+ project.get_subproject_inpath(collectionname)
191
+ )
192
+
193
+ xpath = request.args.get("xpath", "")
194
+
195
+ return render_template(
196
+ "modal/tei_explorer.html",
197
+ projectname=projectname,
198
+ collectionname=collectionname,
199
+ dir_list=dir_list_dict,
200
+ file_list=file_list_dict,
201
+ xpath=xpath,
202
+ project=project,
203
+ )
204
+
205
+
206
+ @collection_routes.route("/load_tei_content", methods=["GET"])
207
+ def load_tei_content():
208
+ path = request.args.get("path")
209
+ log.debug("load_tei_content path: %s" % path)
210
+ _type = request.args.get("type")
211
+ if path is not None:
212
+ tei_parser = TEIParser(fullpath=path)
213
+ if _type == "header":
214
+ return json_response(
215
+ value="OK",
216
+ content=tei_parser.find(
217
+ "//teiHeader", node_as_text=True
218
+ ).decode("utf-8"),
219
+ )
220
+ elif _type == "text":
221
+ return json_response(
222
+ value="OK",
223
+ content=tei_parser.find(".//text", node_as_text=True).decode(
224
+ "utf-8"
225
+ ),
226
+ )
227
+ return json_response(value="Unknown type requested!")
228
+
229
+
230
+ @collection_routes.route(
231
+ "/modal/xpath_parser/<string:projectname>/<string:collectionname>",
232
+ methods=["GET", "POST"],
233
+ )
234
+ def modal_xpath_parser(
235
+ projectname,
236
+ collectionname,
237
+ ):
238
+ project = Project(projectname)
239
+ collection = project.get_collection(
240
+ collectionname,
241
+ )
242
+ collection_parser = collection["parser"]
243
+
244
+ xpath = request.args.get("xpath")
245
+ if not xpath:
246
+ xpath = request.form.get("xpath", "")
247
+
248
+ return render_template(
249
+ "modal/xpath_parser.html",
250
+ xpath=xpath,
251
+ projectname=projectname,
252
+ collectionname=collectionname,
253
+ collection_parser=collection_parser,
254
+ )
255
+
256
+
257
+ @collection_routes.route("/export_tsv/<projectname>/<collectionname>")
258
+ def export_tsv(projectname, collectionname):
259
+ project = Project(projectname)
260
+ collection = project.get_collection(
261
+ collectionname,
262
+ )
263
+ filepath = collection["modeler"].export()
264
+ if os.path.isfile(filepath):
265
+ with open(filepath, "rb") as file:
266
+ file_content = BytesIO(file.read())
267
+
268
+ return send_file(
269
+ file_content,
270
+ download_name=os.path.basename(filepath),
271
+ as_attachment=True,
272
+ )
tgp_ui/routes/data.py ADDED
@@ -0,0 +1,228 @@
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 shutil
10
+
11
+ from flask import Blueprint, render_template, request, send_file, session
12
+ from flask_json import json_response
13
+
14
+ from io import BytesIO
15
+
16
+ from tgp_backend.config import MAIN_PATH
17
+ from tgp_backend.nextcloud import Nextcloud
18
+ from tgp_backend.project import Project
19
+ from tgp_backend.session_manager import SessionManagerNextcloud
20
+
21
+ log = logging.getLogger(__name__)
22
+
23
+ data_routes = Blueprint("data", __name__)
24
+
25
+
26
+ # ***TABS***
27
+ # **********
28
+ @data_routes.route("/tab-import-data/<string:projectname>", methods=["GET"])
29
+ def tab_import_data(projectname=None, project=None, error_message=None):
30
+ if not project:
31
+ if not projectname:
32
+ raise ValueError("Project name must be provided.")
33
+ else:
34
+ project = Project(projectname)
35
+ return render_template(
36
+ "tabs/import_data.html",
37
+ project=project,
38
+ error_message=error_message,
39
+ )
40
+
41
+
42
+ # ***DIRECT UPLOAD***
43
+ # *******************
44
+ @data_routes.route("/upload_files/<string:projectname>", methods=["POST"])
45
+ def upload_files(projectname=None):
46
+ project = Project(projectname)
47
+ project.file_upload(request.files.getlist("files"))
48
+
49
+ return tab_import_data(project=project)
50
+
51
+
52
+ # ***GITHUB***
53
+ # ************
54
+ @data_routes.route("/modal/github/<projectname>", methods=["GET", "POST"])
55
+ def modal_github(projectname):
56
+ """
57
+ Render the Github modal.
58
+ """
59
+ project = Project(projectname)
60
+ return render_template(
61
+ "modal/github_modal.html",
62
+ project=project,
63
+ )
64
+
65
+
66
+ @data_routes.route("/clone_git_project/<string:projectname>", methods=["POST"])
67
+ def clone_git_project(projectname=None):
68
+ project = Project(projectname)
69
+ project.clone_git_project(request.form.get("github_repo"))
70
+
71
+ return tab_import_data(project=project)
72
+
73
+
74
+ # ***NEXTCLOUD***
75
+ # ***************
76
+
77
+
78
+ def render_nextcloud_tree(projectname, items):
79
+ """
80
+ Render the Nextcloud tree view.
81
+ """
82
+ # items = items or nc.nxc_list_files_and_folders()
83
+ project = Project(projectname)
84
+ return render_template(
85
+ "modal/file_explorer_nextcloud.html",
86
+ project=project,
87
+ items=items,
88
+ show_checkbox=True,
89
+ show_all_folders=True,
90
+ )
91
+
92
+
93
+ def render_nextcloud_login(projectname, message=None):
94
+ """
95
+ Render the Nextcloud login form.
96
+ """
97
+ return render_template(
98
+ "modal/nextcloud_login.html", projectname=projectname, message=message
99
+ )
100
+
101
+
102
+ @data_routes.route("/modal/nextcloud/<projectname>", methods=["GET", "POST"])
103
+ @data_routes.route(
104
+ "/modal/nextcloud/<projectname>/<logout>", methods=["GET", "POST"]
105
+ )
106
+ def modal_nextcloud(projectname, form_type=None, logout=False):
107
+
108
+ nxc_session_manager = SessionManagerNextcloud(session)
109
+
110
+ if logout:
111
+ # If the user wants to log out, delete the session credentials
112
+ nxc_session_manager.delete_nextcloud_credentials()
113
+ return render_nextcloud_login(projectname, message="You logged out.")
114
+
115
+ nc = Nextcloud(**nxc_session_manager.get_credentials())
116
+ if nc.test_connection():
117
+ # An active Nextcloud session exists, so we can use it to render
118
+ # the tree view
119
+ return render_nextcloud_tree(
120
+ projectname, nc.nxc_list_files_and_folders()
121
+ )
122
+ elif nxc_session_manager.request_has_valid_credentials(request):
123
+ # If credentials are provided,
124
+ # Recreate a Nextcloud instance and test the connection
125
+ nxc_session_manager.save_credentials(request.form)
126
+ nc = Nextcloud(**nxc_session_manager.get_credentials())
127
+ if nc.test_connection():
128
+ return render_nextcloud_tree(
129
+ projectname, nc.nxc_list_files_and_folders()
130
+ )
131
+
132
+ else:
133
+ return render_nextcloud_login(
134
+ projectname,
135
+ message="Login failed. Please check your credentials.",
136
+ )
137
+ else:
138
+ # If no valid credentials are provided, render the login form
139
+ log.info("No valid Nextcloud credentials found, showing login form.")
140
+ # This will clear any previous session data
141
+ nxc_session_manager.delete_nextcloud_credentials()
142
+ return render_nextcloud_login(projectname)
143
+
144
+
145
+ @data_routes.route("/download_nextcloud/<projectname>", methods=["POST"])
146
+ def download_nextcloud(projectname):
147
+
148
+ nxc_session_manager = SessionManagerNextcloud(session)
149
+
150
+ selected_option = request.form.getlist("selected_folder")
151
+
152
+ if selected_option:
153
+ nc = Nextcloud(**nxc_session_manager.get_credentials())
154
+ nc.download_nxc_files(selected_option, projectname=projectname)
155
+
156
+ return json_response(
157
+ status=200,
158
+ message="Downloaded.",
159
+ )
160
+ else:
161
+ return json_response(
162
+ status=400, message="No folder selected for download."
163
+ )
164
+
165
+
166
+ # ***OTHER***
167
+ # *******************
168
+ @data_routes.route("/image/<path:filepath>")
169
+ def serve_image(filepath):
170
+ filepath = "/" + filepath
171
+ if os.path.isfile(filepath):
172
+ return send_file(filepath)
173
+ return "", 404
174
+
175
+
176
+ @data_routes.route("/file/<path:filepath>")
177
+ def serve_file(filepath):
178
+
179
+ filepath = "/" + filepath
180
+
181
+ if os.path.isfile(filepath):
182
+ with open(filepath, "rb") as file:
183
+ file_content = BytesIO(file.read())
184
+
185
+ return send_file(
186
+ file_content,
187
+ download_name=os.path.basename(filepath),
188
+ as_attachment=True,
189
+ )
190
+
191
+
192
+ @data_routes.route("/api/delete-folder", methods=["DELETE"])
193
+ def delete_folder():
194
+ error_message = None # Variable für die Fehlermeldung
195
+ try:
196
+ path = request.form.get("path")
197
+ projectname = request.form.get("projectname")
198
+
199
+ if not path:
200
+ error_message = "No path specified"
201
+ elif not path.startswith(MAIN_PATH):
202
+ # Security check to ensure path is within allowed directory
203
+ error_message = "Invalid path"
204
+ elif not os.path.exists(path):
205
+ # Check if path exists
206
+ error_message = "Path not found"
207
+ else:
208
+ # Check if the path is a file or a folder
209
+ if os.path.isfile(path):
210
+ # Delete the file
211
+ os.remove(path)
212
+ log.info(f"File deleted: {path}")
213
+ elif os.path.isdir(path):
214
+ # Delete the folder and its contents
215
+ shutil.rmtree(path)
216
+ log.info(f"Folder deleted: {path}")
217
+ else:
218
+ error_message = "Path is neither a file nor a folder"
219
+
220
+ except Exception as e:
221
+ log.error(f"Error deleting path: {e}")
222
+ error_message = f"An error occurred: {str(e)}"
223
+
224
+ # Am Ende einmal `tab_import_data` aufrufen
225
+ return tab_import_data(
226
+ projectname=projectname,
227
+ error_message=error_message,
228
+ )
@@ -0,0 +1,102 @@
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 shutil
10
+
11
+ from flask import Blueprint, redirect, render_template, request
12
+ from flask import url_for as default_url_for
13
+ from flask_json import json_response
14
+
15
+
16
+ from tgp_backend.config import MAIN_PATH
17
+ from tgp_backend.project import Project
18
+
19
+ from .views import overview
20
+
21
+ log = logging.getLogger(__name__)
22
+
23
+ project_routes = Blueprint("project", __name__)
24
+
25
+
26
+ def get_prefix():
27
+ return request.headers.get(
28
+ "X-Forwarded-Prefix", request.headers.get("X-Script-Name", "")
29
+ )
30
+
31
+
32
+ def url_for(*args, **kwargs):
33
+ """Overrides Flask's url_for globally to include the prefix"""
34
+ return get_prefix() + default_url_for(*args, **kwargs)
35
+
36
+
37
+ @project_routes.route("/new_project", methods=["POST"])
38
+ def new_project():
39
+ project = Project(request.form.get("projectname"))
40
+ project.create()
41
+ return redirect(url_for("views.project_view", projectname=project.path))
42
+
43
+
44
+ @project_routes.route("/delete_project/<string:projectname>", methods=["POST"])
45
+ def delete_project(projectname):
46
+
47
+ # projectname = request.form.get("projectname")
48
+ fullpath = f"{MAIN_PATH}/{projectname}"
49
+
50
+ # Delete the project
51
+ if fullpath.strip("/") == MAIN_PATH.strip("/"):
52
+ log.error(f"Cannot delete main path ({MAIN_PATH})!")
53
+ elif os.path.exists(fullpath):
54
+ shutil.rmtree(fullpath)
55
+ else:
56
+ log.warning("Project does not exist!")
57
+
58
+ return overview()
59
+
60
+
61
+ @project_routes.route(
62
+ "/modal_delete_project/<string:projectname>", methods=["POST"]
63
+ )
64
+ def modal_delete_project(projectname):
65
+ return render_template(
66
+ "modal/delete_project.html",
67
+ projectname=projectname,
68
+ )
69
+
70
+
71
+ @project_routes.route(
72
+ "/tab-edit-project/<string:projectname>", methods=["GET", "POST"]
73
+ )
74
+ def tab_edit_project(projectname):
75
+ project = Project(projectname)
76
+ if request.method == "POST":
77
+ collectors = []
78
+ for name, url in zip(
79
+ request.form.getlist("collector_name"),
80
+ request.form.getlist("collector_url"),
81
+ ):
82
+ collectors.append({"name": name, "url": url})
83
+
84
+ project.update(
85
+ title=request.form["title"],
86
+ description=request.form["description"],
87
+ collectors=collectors,
88
+ avatar=request.files["avatar"],
89
+ xslt=request.files.get("xslt"),
90
+ )
91
+ return render_template(
92
+ "tabs/edit_project.html",
93
+ project=project,
94
+ )
95
+
96
+
97
+ @project_routes.route("/delete-xslt/<string:projectname>", methods=["DELETE"])
98
+ def delete_xslt(projectname):
99
+ project = Project(projectname)
100
+ log.debug(f"Deleting XSLT for project {projectname}")
101
+ project.clear_xslt()
102
+ return json_response("OK")