From f1f33e2fe42b7e91b77a6e56eb8f2f37a7bceb1e Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Wed, 4 Mar 2026 21:29:01 +0000 Subject: [PATCH 1/7] feat: implement delete dataset route --- server/app.py | 20 ++++++++++++++++++++ server/core/datasets.py | 18 +++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/server/app.py b/server/app.py index 1332ad2..f490fe9 100644 --- a/server/app.py +++ b/server/app.py @@ -175,6 +175,26 @@ def get_dataset(dataset_id): except Exception: print(traceback.format_exc()) return jsonify({"error": "An unexpected error occured"}), 500 + +@app.route("/dataset/", methods=["DELETE"]) +@jwt_required() +def delete_dataset(dataset_id): + try: + user_id = int(get_jwt_identity()) + + if not dataset_manager.authorize_user_dataset(dataset_id, user_id): + raise NotAuthorisedException("This user is not authorised to access this dataset") + + dataset_manager.delete_dataset_info(dataset_id) + dataset_manager.delete_dataset_content(dataset_id) + return jsonify({"message": f"Dataset {dataset_id} metadata and content successfully deleted"}), 200 + except NotAuthorisedException: + return jsonify({"error": "User is not authorised to access this content"}), 403 + except NonExistentDatasetException: + return jsonify({"error": "Dataset does not exist"}), 404 + except Exception: + print(traceback.format_exc()) + return jsonify({"error": "An unexpected error occured"}), 500 @app.route("/dataset//status", methods=["GET"]) @jwt_required() diff --git a/server/core/datasets.py b/server/core/datasets.py index e7ee717..2edb42c 100644 --- a/server/core/datasets.py +++ b/server/core/datasets.py @@ -143,4 +143,20 @@ class DatasetManager: print(result) raise NonExistentDatasetException(f"Dataset {dataset_id} does not exist") - return result[0] \ No newline at end of file + return result[0] + + def delete_dataset_info(self, dataset_id: int): + query = """ + DELETE FROM datasets + WHERE id = %s + """ + + self.db.execute(query, (dataset_id, )) + + def delete_dataset_content(self, dataset_id: int): + query = """ + DELETE FROM events + WHERE id = %s + """ + + self.db.execute(query, (dataset_id, )) \ No newline at end of file From 4f01bf041976edd0b70120c9d205d6baecbe759c Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Wed, 4 Mar 2026 21:35:10 +0000 Subject: [PATCH 2/7] fix(db): incorrect SQL condition when deleting dataset content --- server/core/datasets.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/server/core/datasets.py b/server/core/datasets.py index 2edb42c..9b15f74 100644 --- a/server/core/datasets.py +++ b/server/core/datasets.py @@ -146,17 +146,11 @@ class DatasetManager: return result[0] def delete_dataset_info(self, dataset_id: int): - query = """ - DELETE FROM datasets - WHERE id = %s - """ + query = "DELETE FROM datasets WHERE id = %s" self.db.execute(query, (dataset_id, )) def delete_dataset_content(self, dataset_id: int): - query = """ - DELETE FROM events - WHERE id = %s - """ + query = "DELETE FROM events WHERE dataset_id = %s" self.db.execute(query, (dataset_id, )) \ No newline at end of file From 64e3f9eea84ebd3aaf9b2c224ba6d89888764d4a Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Wed, 4 Mar 2026 21:38:06 +0000 Subject: [PATCH 3/7] feat: implement PATCH dataset route At the moment only allows for the updating of the name. Which seems to be the only editable part of dataset metadata. --- server/app.py | 25 +++++++++++++++++++++++++ server/core/datasets.py | 4 ++++ 2 files changed, 29 insertions(+) diff --git a/server/app.py b/server/app.py index f490fe9..53141cc 100644 --- a/server/app.py +++ b/server/app.py @@ -176,6 +176,31 @@ def get_dataset(dataset_id): print(traceback.format_exc()) return jsonify({"error": "An unexpected error occured"}), 500 +@app.route("/dataset/", methods=["PATCH"]) +@jwt_required() +def update_dataset(dataset_id): + try: + user_id = int(get_jwt_identity()) + + if not dataset_manager.authorize_user_dataset(dataset_id, user_id): + raise NotAuthorisedException("This user is not authorised to access this dataset") + + body = request.get_json() + new_name = body.get("name") + + if not new_name or not new_name.strip(): + return jsonify({"error": "A valid name must be provided"}), 400 + + dataset_manager.update_dataset_name(dataset_id, new_name.strip()) + return jsonify({"message": f"Dataset {dataset_id} renamed to '{new_name.strip()}'"}), 200 + except NotAuthorisedException: + return jsonify({"error": "User is not authorised to access this content"}), 403 + except NonExistentDatasetException: + return jsonify({"error": "Dataset does not exist"}), 404 + except Exception: + print(traceback.format_exc()) + return jsonify({"error": "An unexpected error occurred"}), 500 + @app.route("/dataset/", methods=["DELETE"]) @jwt_required() def delete_dataset(dataset_id): diff --git a/server/core/datasets.py b/server/core/datasets.py index 9b15f74..5886cfc 100644 --- a/server/core/datasets.py +++ b/server/core/datasets.py @@ -145,6 +145,10 @@ class DatasetManager: return result[0] + def update_dataset_name(self, dataset_id: int, new_name: str): + query = "UPDATE datasets SET name = %s WHERE id = %s" + self.db.execute(query, (new_name, dataset_id)) + def delete_dataset_info(self, dataset_id: int): query = "DELETE FROM datasets WHERE id = %s" From f5835b5a97ba20a2ad6a9b18442fac275f5417af Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Wed, 4 Mar 2026 22:17:31 +0000 Subject: [PATCH 4/7] feat(frontend): add frontend option to change name --- frontend/src/App.tsx | 2 + frontend/src/pages/DatasetEdit.tsx | 161 +++++++++++++++++++++++++++++ frontend/src/pages/Datasets.tsx | 27 +++-- server/app.py | 20 +++- 4 files changed, 199 insertions(+), 11 deletions(-) create mode 100644 frontend/src/pages/DatasetEdit.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a017f87..b1e6045 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -7,6 +7,7 @@ import LoginPage from "./pages/Login"; import UploadPage from "./pages/Upload"; import StatPage from "./pages/Stats"; import { getDocumentTitle } from "./utils/documentTitle"; +import DatasetEditPage from "./pages/DatasetEdit"; function App() { const location = useLocation(); @@ -24,6 +25,7 @@ function App() { } /> } /> } /> + } /> ); diff --git a/frontend/src/pages/DatasetEdit.tsx b/frontend/src/pages/DatasetEdit.tsx new file mode 100644 index 0000000..dc3a818 --- /dev/null +++ b/frontend/src/pages/DatasetEdit.tsx @@ -0,0 +1,161 @@ +import StatsStyling from "../styles/stats_styling"; +import { useNavigate, useParams } from "react-router-dom"; +import { useEffect, useMemo, useState, type FormEvent } from "react"; +import axios from "axios"; + +const API_BASE_URL = import.meta.env.VITE_BACKEND_URL; +const styles = StatsStyling; + +type DatasetInfoResponse = { + id: number; + name: string; + created_at: string; +}; + +const DatasetEditPage = () => { + const navigate = useNavigate(); + const { datasetId } = useParams<{ datasetId: string }>(); + const parsedDatasetId = useMemo(() => Number(datasetId), [datasetId]); + const [statusMessage, setStatusMessage] = useState(""); + const [loading, setLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [hasError, setHasError] = useState(false); + + const [datasetName, setDatasetName] = useState(""); + + useEffect(() => { + if (!Number.isInteger(parsedDatasetId) || parsedDatasetId <= 0) { + setHasError(true); + setStatusMessage("Invalid dataset id."); + setLoading(false); + return; + } + + const token = localStorage.getItem("access_token"); + if (!token) { + setHasError(true); + setStatusMessage("You must be signed in to edit datasets."); + setLoading(false); + return; + } + + axios + .get(`${API_BASE_URL}/dataset/${parsedDatasetId}`, { + headers: { Authorization: `Bearer ${token}` }, + }) + .then((response) => { + setDatasetName(response.data.name || ""); + }) + .catch((error: unknown) => { + setHasError(true); + if (axios.isAxiosError(error)) { + setStatusMessage(String(error.response?.data?.error || error.message)); + } else { + setStatusMessage("Could not get dataset info."); + } + }) + .finally(() => { + setLoading(false); + }); + }, [parsedDatasetId]); + + const saveDatasetName = async (event: FormEvent) => { + event.preventDefault(); + + const trimmedName = datasetName.trim(); + if (!trimmedName) { + setHasError(true); + setStatusMessage("Please enter a valid dataset name."); + return; + } + + const token = localStorage.getItem("access_token"); + if (!token) { + setHasError(true); + setStatusMessage("You must be signed in to save changes."); + return; + } + + try { + setIsSaving(true); + setHasError(false); + setStatusMessage(""); + + await axios.patch( + `${API_BASE_URL}/dataset/${parsedDatasetId}`, + { name: trimmedName }, + { headers: { Authorization: `Bearer ${token}` } } + ); + + navigate("/datasets", { replace: true }); + } catch (error: unknown) { + setHasError(true); + if (axios.isAxiosError(error)) { + setStatusMessage(String(error.response?.data?.error || error.message || "Save failed.")); + } else { + setStatusMessage("Save failed due to an unexpected error."); + } + } finally { + setIsSaving(false); + } + }; + + return ( +
+
+
+
+

Edit Dataset

+

Update the dataset name shown in your datasets list.

+
+
+ +
+ + + setDatasetName(event.target.value)} + disabled={loading || isSaving} + /> + +
+ + + + {loading + ? "Loading dataset details..." + : statusMessage} +
+
+
+
+ ); +}; + +export default DatasetEditPage; diff --git a/frontend/src/pages/Datasets.tsx b/frontend/src/pages/Datasets.tsx index b054420..4c79cdc 100644 --- a/frontend/src/pages/Datasets.tsx +++ b/frontend/src/pages/Datasets.tsx @@ -94,6 +94,7 @@ const DatasetsPage = () => {
    {datasets.map((dataset) => { const isComplete = dataset.status === "complete"; + const editPath = `/dataset/${dataset.id}/edit`; const targetPath = isComplete ? `/dataset/${dataset.id}/stats` : `/dataset/${dataset.id}/status`; @@ -117,13 +118,25 @@ const DatasetsPage = () => { )} - +
    + { isComplete && + + } + + +
    ); })} diff --git a/server/app.py b/server/app.py index 53141cc..7cbf9d3 100644 --- a/server/app.py +++ b/server/app.py @@ -164,10 +164,10 @@ def get_dataset(dataset_id): if not dataset_manager.authorize_user_dataset(dataset_id, user_id): raise NotAuthorisedException("This user is not authorised to access this dataset") - dataset_content = dataset_manager.get_dataset_content(dataset_id) - filters = get_request_filters() - filtered_dataset = stat_gen.filter_dataset(dataset_content, filters) - return jsonify(filtered_dataset), 200 + dataset_info = dataset_manager.get_dataset_info(dataset_id) + included_cols = {"id", "name", "created_at"} + + return jsonify({k: dataset_info[k] for k in included_cols}), 200 except NotAuthorisedException: return jsonify({"error": "User is not authorised to access this content"}), 403 except NonExistentDatasetException: @@ -253,6 +253,8 @@ def content_endpoint(dataset_id): return jsonify(stat_gen.get_content_analysis(dataset_content, filters)), 200 except NotAuthorisedException: return jsonify({"error": "User is not authorised to access this content"}), 403 + except NonExistentDatasetException: + return jsonify({"error": "Dataset does not exist"}), 404 except ValueError as e: return jsonify({"error": f"Malformed or missing data: {str(e)}"}), 400 except Exception as e: @@ -273,6 +275,8 @@ def get_summary(dataset_id): return jsonify(stat_gen.summary(dataset_content, filters)), 200 except NotAuthorisedException: return jsonify({"error": "User is not authorised to access this content"}), 403 + except NonExistentDatasetException: + return jsonify({"error": "Dataset does not exist"}), 404 except ValueError as e: return jsonify({"error": f"Malformed or missing data: {str(e)}"}), 400 except Exception as e: @@ -293,6 +297,8 @@ def get_time_analysis(dataset_id): return jsonify(stat_gen.get_time_analysis(dataset_content, filters)), 200 except NotAuthorisedException: return jsonify({"error": "User is not authorised to access this content"}), 403 + except NonExistentDatasetException: + return jsonify({"error": "Dataset does not exist"}), 404 except ValueError as e: return jsonify({"error": f"Malformed or missing data: {str(e)}"}), 400 except Exception as e: @@ -313,6 +319,8 @@ def get_user_analysis(dataset_id): return jsonify(stat_gen.get_user_analysis(dataset_content, filters)), 200 except NotAuthorisedException: return jsonify({"error": "User is not authorised to access this content"}), 403 + except NonExistentDatasetException: + return jsonify({"error": "Dataset does not exist"}), 404 except ValueError as e: return jsonify({"error": f"Malformed or missing data: {str(e)}"}), 400 except Exception as e: @@ -333,6 +341,8 @@ def get_cultural_analysis(dataset_id): return jsonify(stat_gen.get_cultural_analysis(dataset_content, filters)), 200 except NotAuthorisedException: return jsonify({"error": "User is not authorised to access this content"}), 403 + except NonExistentDatasetException: + return jsonify({"error": "Dataset does not exist"}), 404 except ValueError as e: return jsonify({"error": f"Malformed or missing data: {str(e)}"}), 400 except Exception as e: @@ -353,6 +363,8 @@ def get_interaction_analysis(dataset_id): return jsonify(stat_gen.get_interactional_analysis(dataset_content, filters)), 200 except NotAuthorisedException: return jsonify({"error": "User is not authorised to access this content"}), 403 + except NonExistentDatasetException: + return jsonify({"error": "Dataset does not exist"}), 404 except ValueError as e: return jsonify({"error": f"Malformed or missing data: {str(e)}"}), 400 except Exception as e: From eec8f2417eb85d77a2f59efd500f46f377ad6779 Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Wed, 4 Mar 2026 22:32:12 +0000 Subject: [PATCH 5/7] feat(frontend): add ability to delete datasets --- frontend/src/pages/DatasetEdit.tsx | 33 ++++++++++++++++++++++++ frontend/src/styles/stats/foundations.ts | 10 +++++++ 2 files changed, 43 insertions(+) diff --git a/frontend/src/pages/DatasetEdit.tsx b/frontend/src/pages/DatasetEdit.tsx index dc3a818..1f21ce1 100644 --- a/frontend/src/pages/DatasetEdit.tsx +++ b/frontend/src/pages/DatasetEdit.tsx @@ -23,6 +23,12 @@ const DatasetEditPage = () => { const [datasetName, setDatasetName] = useState(""); + const token = localStorage.getItem("access_token"); + if (!token) { + setHasError(true); + setStatusMessage("You must be signed in to save changes."); + } + useEffect(() => { if (!Number.isInteger(parsedDatasetId) || parsedDatasetId <= 0) { setHasError(true); @@ -59,6 +65,7 @@ const DatasetEditPage = () => { }); }, [parsedDatasetId]); + const saveDatasetName = async (event: FormEvent) => { event.preventDefault(); @@ -100,6 +107,23 @@ const DatasetEditPage = () => { } }; + const deleteDataset = async () => { + try{ + await axios.delete( + `${API_BASE_URL}/dataset/${parsedDatasetId}`, + { headers: { Authorization: `Bearer ${token}` } } + ); + navigate("/datasets", { replace: true }); + } catch (error: unknown) { + setHasError(true); + if (axios.isAxiosError(error)) { + setStatusMessage(String(error.response?.data?.error || error.message || "Save failed.")); + } else { + setStatusMessage("Save failed due to an unexpected error."); + } + } + } + return (
    @@ -132,6 +156,15 @@ const DatasetEditPage = () => { />
    + + + +
    + +
    + + ); +} diff --git a/frontend/src/pages/DatasetEdit.tsx b/frontend/src/pages/DatasetEdit.tsx index 1f21ce1..fb92603 100644 --- a/frontend/src/pages/DatasetEdit.tsx +++ b/frontend/src/pages/DatasetEdit.tsx @@ -2,6 +2,7 @@ import StatsStyling from "../styles/stats_styling"; import { useNavigate, useParams } from "react-router-dom"; import { useEffect, useMemo, useState, type FormEvent } from "react"; import axios from "axios"; +import ConfirmationModal from "../components/ConfirmationModal"; const API_BASE_URL = import.meta.env.VITE_BACKEND_URL; const styles = StatsStyling; @@ -19,16 +20,11 @@ const DatasetEditPage = () => { const [statusMessage, setStatusMessage] = useState(""); const [loading, setLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [hasError, setHasError] = useState(false); const [datasetName, setDatasetName] = useState(""); - - const token = localStorage.getItem("access_token"); - if (!token) { - setHasError(true); - setStatusMessage("You must be signed in to save changes."); - } - useEffect(() => { if (!Number.isInteger(parsedDatasetId) || parsedDatasetId <= 0) { setHasError(true); @@ -108,21 +104,37 @@ const DatasetEditPage = () => { }; const deleteDataset = async () => { - try{ + const deleteToken = localStorage.getItem("access_token"); + if (!deleteToken) { + setHasError(true); + setStatusMessage("You must be signed in to delete datasets."); + setIsDeleteModalOpen(false); + return; + } + + try { + setIsDeleting(true); + setHasError(false); + setStatusMessage(""); + await axios.delete( `${API_BASE_URL}/dataset/${parsedDatasetId}`, - { headers: { Authorization: `Bearer ${token}` } } + { headers: { Authorization: `Bearer ${deleteToken}` } } ); + + setIsDeleteModalOpen(false); navigate("/datasets", { replace: true }); } catch (error: unknown) { setHasError(true); if (axios.isAxiosError(error)) { - setStatusMessage(String(error.response?.data?.error || error.message || "Save failed.")); + setStatusMessage(String(error.response?.data?.error || error.message || "Delete failed.")); } else { - setStatusMessage("Save failed due to an unexpected error."); + setStatusMessage("Delete failed due to an unexpected error."); } + } finally { + setIsDeleting(false); } - } + }; return (
    @@ -159,8 +171,8 @@ const DatasetEditPage = () => { @@ -169,14 +181,14 @@ const DatasetEditPage = () => { type="button" style={styles.buttonSecondary} onClick={() => navigate("/datasets")} - disabled={isSaving} + disabled={isSaving || isDeleting} > Cancel @@ -186,6 +198,17 @@ const DatasetEditPage = () => { : statusMessage}
    + + setIsDeleteModalOpen(false)} + onConfirm={deleteDataset} + />
    );