Skip to content

Commit

Permalink
Add admin buttons to delete and resubmit samples
Browse files Browse the repository at this point in the history
- add DELETE `admin/samples/<sample_id>` API endpoint
  - deletes the sample and all associated input files and results
- add delete button to admin interface with modal confirmation dialog
  - resolves #39
- add POST `admin/resubmit-sample/<sample_id>` API endpoint
  - deletes any existing results for the sample, then sets its status to QUEUED
- add resubmit button to admin interface with modal confirmation dialog
  - resolves #40
  • Loading branch information
lkeegan committed Oct 25, 2024
1 parent 4fb00da commit d8b9477
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 8 deletions.
31 changes: 31 additions & 0 deletions backend/src/predicTCR_server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import os
import secrets
import datetime
import shutil
import flask
from flask import Flask
from flask import jsonify
Expand Down Expand Up @@ -249,6 +250,36 @@ def admin_all_samples():
return jsonify(message="Admin account required"), 400
return jsonify(get_samples())

@app.route("/api/admin/resubmit-sample/<int:sample_id>", methods=["POST"])
@jwt_required()
def admin_resubmit_sample(sample_id: int):
if not current_user.is_admin:
return jsonify(message="Admin account required"), 400
sample = db.session.get(Sample, sample_id)
if sample is None:
return jsonify(message="Sample not found"), 404
sample.result_file_path().unlink(missing_ok=True)
sample.has_results_zip = False
sample.status = Status.QUEUED
db.session.commit()
return jsonify(message="Sample added to the queue")

@app.route("/api/admin/samples/<int:sample_id>", methods=["DELETE"])
@jwt_required()
def admin_delete_sample(sample_id: int):
if not current_user.is_admin:
return jsonify(message="Admin account required"), 400
sample = db.session.get(Sample, sample_id)
if sample is None:
return jsonify(message="Sample not found"), 404
try:
shutil.rmtree(sample.base_path())
except Exception as e:
logger.error(e)
db.session.delete(sample)
db.session.commit()
return jsonify(message="Sample deleted")

@app.route("/api/admin/user", methods=["POST"])
@jwt_required()
def admin_update_user():
Expand Down
8 changes: 4 additions & 4 deletions backend/src/predicTCR_server/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,18 +85,18 @@ class Sample(db.Model):
status: Mapped[Status] = mapped_column(Enum(Status), nullable=False)
has_results_zip: Mapped[bool] = mapped_column(Boolean, nullable=False)

def _base_path(self) -> pathlib.Path:
def base_path(self) -> pathlib.Path:
data_path = flask.current_app.config["PREDICTCR_DATA_PATH"]
return pathlib.Path(f"{data_path}/{self.id}")

def input_h5_file_path(self) -> pathlib.Path:
return self._base_path() / "input.h5"
return self.base_path() / "input.h5"

def input_csv_file_path(self) -> pathlib.Path:
return self._base_path() / "input.csv"
return self.base_path() / "input.csv"

def result_file_path(self) -> pathlib.Path:
return self._base_path() / "result.zip"
return self.base_path() / "result.zip"


@dataclass
Expand Down
5 changes: 3 additions & 2 deletions backend/tests/helpers/flask_test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,15 @@ def add_test_users(app):

def add_test_samples(app, data_path: pathlib.Path):
with app.app_context():
for sample_id, name in zip(
for sample_id, name, status in zip(
[1, 2, 3, 4],
[
"s1",
"s2",
"s3",
"s4",
],
[Status.QUEUED, Status.RUNNING, Status.COMPLETED, Status.FAILED],
):
ref_dir = data_path / f"{sample_id}"
ref_dir.mkdir(parents=True, exist_ok=True)
Expand All @@ -54,7 +55,7 @@ def add_test_samples(app, data_path: pathlib.Path):
source=f"source{sample_id}",
timestamp=sample_id,
timestamp_results=0,
status=Status.QUEUED,
status=status,
has_results_zip=False,
)
db.session.add(new_sample)
Expand Down
28 changes: 28 additions & 0 deletions backend/tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,34 @@ def test_admin_samples_valid(client):
assert len(response.json) == 4


def test_admin_delete_samples_valid_admin_user(client):
headers = _get_auth_headers(client, "admin@abc.xy", "admin")
assert len(client.get("/api/admin/samples", headers=headers).json) == 4
response = client.delete("/api/admin/samples/1", headers=headers)
assert response.status_code == 200
assert len(client.get("/api/admin/samples", headers=headers).json) == 3
response = client.delete("/api/admin/samples/1", headers=headers)
assert response.status_code == 404
response = client.delete("/api/admin/samples/2", headers=headers)
assert response.status_code == 200
assert len(client.get("/api/admin/samples", headers=headers).json) == 2


@pytest.mark.parametrize(
"index,sample_id,status",
[(0, 4, "failed"), (1, 3, "completed"), (2, 2, "running"), (3, 1, "queued")],
)
def test_admin_resubmit_samples_valid_admin_user(client, index, sample_id, status):
headers = _get_auth_headers(client, "admin@abc.xy", "admin")
sample_before = client.get("/api/admin/samples", headers=headers).json[index]
assert sample_before["status"] == status
response = client.post(f"/api/admin/resubmit-sample/{sample_id}", headers=headers)
assert response.status_code == 200
sample_after = client.get("/api/admin/samples", headers=headers).json[index]
assert sample_after["status"] == "queued"
assert sample_after["has_results_zip"] is False


def test_admin_runner_token_invalid(client):
# no auth header
response = client.get("/api/admin/runner_token")
Expand Down
102 changes: 102 additions & 0 deletions frontend/src/components/SamplesTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// @ts-ignore
import {
FwbA,
FwbButton,
FwbModal,
FwbTable,
FwbTableBody,
FwbTableCell,
Expand All @@ -10,16 +12,59 @@ import {
FwbTableRow,
} from "flowbite-vue";
import {
apiClient,
download_input_csv_file,
download_input_h5_file,
download_result,
logout,
} from "@/utils/api-client";
import type { Sample } from "@/utils/types";
import { ref } from "vue";
defineProps<{
samples: Sample[];
admin: boolean;
}>();
const emit = defineEmits(["samplesModified"]);
const current_sample_id = ref(null as number | null);
const show_delete_modal = ref(false);
const show_resubmit_modal = ref(false);
function close_modals() {
show_resubmit_modal.value = false;
show_delete_modal.value = false;
}
function resubmit_current_sample() {
close_modals();
apiClient
.post(`admin/resubmit-sample/${current_sample_id.value}`)
.then(() => {
emit("samplesModified");
})
.catch((error) => {
if (error.response.status > 400) {
logout();
}
console.log(error);
});
}
function delete_current_sample() {
close_modals();
apiClient
.delete(`admin/samples/${current_sample_id.value}`)
.then(() => {
emit("samplesModified");
})
.catch((error) => {
if (error.response.status > 400) {
logout();
}
console.log(error);
});
}
</script>

<template>
Expand All @@ -34,6 +79,7 @@ defineProps<{
<fwb-table-head-cell>Status</fwb-table-head-cell>
<fwb-table-head-cell>Inputs</fwb-table-head-cell>
<fwb-table-head-cell>Results</fwb-table-head-cell>
<fwb-table-head-cell v-if="admin">Actions</fwb-table-head-cell>
</fwb-table-head>
<fwb-table-body>
<fwb-table-row v-for="sample in samples" :key="sample.id">
Expand Down Expand Up @@ -71,7 +117,63 @@ defineProps<{
</template>
<template v-else> - </template>
</fwb-table-cell>
<fwb-table-cell v-if="admin">
<fwb-button
@click="
current_sample_id = sample.id;
show_resubmit_modal = true;
"
class="mr-2"
>Resubmit</fwb-button
>
<fwb-button
@click="
current_sample_id = sample.id;
show_delete_modal = true;
"
class="mr-2"
color="red"
>Delete</fwb-button
>
</fwb-table-cell>
</fwb-table-row>
</fwb-table-body>
</fwb-table>

<fwb-modal size="lg" v-if="show_resubmit_modal" @close="close_modals">
<template #header>
<div class="flex items-center text-lg">Resubmit sample</div>
</template>
<template #body
>Are you sure you want to resubmit this sample (any existing results will
be deleted)?
</template>
<template #footer>
<div class="flex justify-between">
<fwb-button @click="close_modals" color="alternative">
No, cancel
</fwb-button>
<fwb-button @click="resubmit_current_sample" color="green">
Yes, resubmit
</fwb-button>
</div>
</template>
</fwb-modal>

<fwb-modal size="lg" v-if="show_delete_modal" @close="close_modals">
<template #header>
<div class="flex items-center text-lg">Delete sample</div>
</template>
<template #body> Are you sure you want to delete this sample? </template>
<template #footer>
<div class="flex justify-between">
<fwb-button @click="close_modals" color="alternative">
No, cancel
</fwb-button>
<fwb-button @click="delete_current_sample" color="red">
Yes, delete
</fwb-button>
</div>
</template>
</fwb-modal>
</template>
6 changes: 5 additions & 1 deletion frontend/src/views/AdminView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,11 @@ get_samples();
<UsersTable :is_runner="false"></UsersTable>
</ListItem>
<ListItem title="Samples">
<SamplesTable :samples="samples" :admin="true"></SamplesTable>
<SamplesTable
:samples="samples"
:admin="true"
@samples-modified="get_samples"
></SamplesTable>
</ListItem>
<ListItem title="Runner Jobs">
<JobsTable />
Expand Down
1 change: 0 additions & 1 deletion runner/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,3 @@ services:
networks:
predictcr-network:
name: predictcr
external: true

0 comments on commit d8b9477

Please sign in to comment.