Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add admin buttons to delete and resubmit samples #45

Merged
merged 1 commit into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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, "[email protected]", "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, "[email protected]", "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
Loading