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

Implement upload of results #46

Merged
merged 1 commit into from
Oct 28, 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
40 changes: 20 additions & 20 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,23 +110,23 @@ jobs:
steps:
- uses: actions/checkout@v4
- run: docker compose build
# - uses: docker/login-action@v3
# with:
# registry: ghcr.io
# username: ${{ github.actor }}
# password: ${{ secrets.GITHUB_TOKEN }}
# if: github.event_name == 'push' && github.ref == 'refs/heads/main'
# - run: |
# echo $PREDICTCR_DOCKER_IMAGE_TAG
# docker compose build
# docker compose push
# if: github.event_name == 'push' && github.ref == 'refs/heads/main'
# env:
# PREDICTCR_DOCKER_IMAGE_TAG: ${{ github.sha }}
# - run: |
# echo $PREDICTCR_DOCKER_IMAGE_TAG
# docker compose build
# docker compose push
# if: github.event_name == 'push' && github.ref == 'refs/heads/main'
# env:
# PREDICTCR_DOCKER_IMAGE_TAG: "latest"
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
- run: |
echo $PREDICTCR_DOCKER_IMAGE_TAG
docker compose build
docker compose push
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
env:
PREDICTCR_DOCKER_IMAGE_TAG: ${{ github.sha }}
- run: |
echo $PREDICTCR_DOCKER_IMAGE_TAG
docker compose build
docker compose push
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
env:
PREDICTCR_DOCKER_IMAGE_TAG: "latest"
47 changes: 41 additions & 6 deletions backend/src/predicTCR_server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,10 @@ def result():
if not user_sample.has_results_zip:
logger.info(f" -> sample {sample_id} found but no results available")
return jsonify(message="No results available"), 400
requested_file = user_sample.result_file_path()
if current_user.full_results:
requested_file = user_sample.trusted_user_result_file_path()
else:
requested_file = user_sample.user_result_file_path()
if not requested_file.is_file():
logger.info(f" -> file {requested_file} not found")
return jsonify(message="Results file not found"), 400
Expand Down Expand Up @@ -243,6 +246,26 @@ def add_sample():
return jsonify(sample=new_sample)
return jsonify(message=error_message), 400

@app.route("/api/admin/result", methods=["POST"])
@jwt_required()
def admin_result():
if not current_user.is_admin:
return jsonify(message="Admin account required"), 400
sample_id = request.json.get("sample_id", None)
logger.info(
f"User {current_user.email} requesting admin results for sample {sample_id}"
)
user_sample = db.session.get(Sample, sample_id)
if user_sample is None:
logger.info(f" -> sample {sample_id} not found")
return jsonify(message="Sample not found"), 400
requested_file = user_sample.admin_result_file_path()
if not requested_file.is_file():
logger.info(f" -> file {requested_file} not found")
return jsonify(message="Results file not found"), 400
logger.info(f"Returning file {requested_file}")
return flask.send_file(requested_file, as_attachment=True)

@app.route("/api/admin/samples", methods=["GET"])
@jwt_required()
def admin_all_samples():
Expand All @@ -258,7 +281,9 @@ def admin_resubmit_sample(sample_id: int):
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.user_result_file_path().unlink(missing_ok=True)
sample.trusted_user_result_file_path().unlink(missing_ok=True)
sample.admin_result_file_path().unlink(missing_ok=True)
sample.has_results_zip = False
sample.status = Status.QUEUED
db.session.commit()
Expand Down Expand Up @@ -381,10 +406,14 @@ def runner_result():
logger.info(" -> missing success key")
return jsonify(message="Missing key: success=True/False"), 400
success = success.lower() == "true"
zipfile = request.files.to_dict().get("file", None)
if success is True and zipfile is None:
user_zipfile = request.files.to_dict().get("user_results", None)
trusted_user_zipfile = request.files.to_dict().get("trusted_user_results", None)
admin_zipfile = request.files.to_dict().get("admin_results", None)
if success is True and (user_zipfile is None or admin_zipfile is None):
logger.info(" -> missing zipfile")
return jsonify(message="Result has success=True but no file"), 400
return jsonify(
message="Result has success=True but a result zipfile is missing"
), 400
runner_hostname = form_as_dict.get("runner_hostname", "")
logger.info(
f"Job '{job_id}' uploaded result for '{sample_id}' from runner {current_user.email} / {runner_hostname}"
Expand All @@ -393,7 +422,13 @@ def runner_result():
if error_message != "":
logger.info(f" -> error message: {error_message}")
message, code = process_result(
int(job_id), int(sample_id), success, error_message, zipfile
int(job_id),
int(sample_id),
success,
error_message,
user_zipfile,
trusted_user_zipfile,
admin_zipfile,
)
return jsonify(message=message), code

Expand Down
44 changes: 28 additions & 16 deletions backend/src/predicTCR_server/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,14 @@ def input_h5_file_path(self) -> pathlib.Path:
def input_csv_file_path(self) -> pathlib.Path:
return self.base_path() / "input.csv"

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

def trusted_user_result_file_path(self) -> pathlib.Path:
return self.base_path() / "trusted_user_results.zip"

def admin_result_file_path(self) -> pathlib.Path:
return self.base_path() / "admin_results.zip"


@dataclass
Expand Down Expand Up @@ -191,37 +197,43 @@ def process_result(
sample_id: int,
success: bool,
error_message: str,
result_zip_file: FileStorage | None,
user_result_zip_file: FileStorage | None,
trusted_user_result_zip_file: FileStorage | None,
admin_result_zip_file: FileStorage | None,
) -> tuple[str, int]:
sample = db.session.get(Sample, sample_id)
if sample is None:
logger.warning(f" --> Unknown sample id {sample_id}")
return f"Unknown sample id {sample_id}", 400
sample.base_path().mkdir(parents=True, exist_ok=True)
job = db.session.get(Job, job_id)
if job is None:
logger.warning(f" --> Unknown job id {job_id}")
return f"Unknown job id {job_id}", 400
job.timestamp_end = timestamp_now()
if success is False:
sample.has_results_zip = False
sample.status = Status.FAILED
if success:
job.status = Status.COMPLETED
else:
job.status = Status.FAILED
job.error_message = error_message
db.session.commit()
return "Result processed", 200
db.session.commit()
if sample.has_results_zip:
logger.warning(f" --> Sample {sample_id} already has results")
job.status = Status.COMPLETED
db.session.commit()
return f"Sample {sample_id} already has results", 400
if result_zip_file is None:
logger.warning(" --> No zipfile")
return "Zip file missing", 400
sample.result_file_path().parent.mkdir(parents=True, exist_ok=True)
result_zip_file.save(sample.result_file_path())
if admin_result_zip_file is not None:
admin_result_zip_file.save(sample.admin_result_file_path())
if success is False:
sample.has_results_zip = False
sample.status = Status.FAILED
db.session.commit()
return "Result processed", 200
if user_result_zip_file is None or trusted_user_result_zip_file is None:
logger.warning(" --> Missing user result zipfile")
return "User result zip file missing", 400
user_result_zip_file.save(sample.user_result_file_path())
trusted_user_result_zip_file.save(sample.trusted_user_result_file_path())
sample.has_results_zip = True
sample.status = Status.COMPLETED
job.status = Status.COMPLETED
db.session.commit()
return "Result processed", 200

Expand Down
4 changes: 3 additions & 1 deletion backend/tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,9 @@ def _upload_result(client, result_zipfile: pathlib.Path, job_id: int, sample_id:
"job_id": job_id,
"sample_id": sample_id,
"success": True,
"file": (io.BytesIO(f.read()), result_zipfile.name),
"user_results": (io.BytesIO(f.read()), result_zipfile.name),
"trusted_user_results": (io.BytesIO(f.read()), result_zipfile.name),
"admin_results": (io.BytesIO(f.read()), result_zipfile.name),
},
headers=headers,
)
Expand Down
16 changes: 14 additions & 2 deletions frontend/src/components/SamplesTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
download_input_csv_file,
download_input_h5_file,
download_result,
download_admin_result,
logout,
} from "@/utils/api-client";
import type { Sample } from "@/utils/types";
Expand Down Expand Up @@ -82,7 +83,11 @@ function delete_current_sample() {
<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">
<fwb-table-row
v-for="sample in samples"
:key="sample.id"
:class="sample.status === 'failed' ? '!bg-red-200' : '!bg-slate-50'"
>
<fwb-table-cell v-if="admin">{{ sample["id"] }}</fwb-table-cell>
<fwb-table-cell>{{
new Date(sample["timestamp"] * 1000).toLocaleDateString("de-DE")
Expand All @@ -108,7 +113,14 @@ function delete_current_sample() {
</fwb-a>
</fwb-table-cell>
<fwb-table-cell>
<template v-if="sample.has_results_zip">
<template v-if="admin">
<fwb-a
href=""
@click.prevent="download_admin_result(sample.id, sample.name)"
>zip</fwb-a
>
</template>
<template v-else-if="sample.has_results_zip">
<fwb-a
href=""
@click.prevent="download_result(sample.id, sample.name)"
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/utils/api-client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import axios from "axios";
import router from "@/router";

Check warning on line 2 in frontend/src/utils/api-client.ts

View workflow job for this annotation

GitHub Actions / Frontend :: node 22

'router' is defined but never used
import type { AxiosInstance } from "axios";
import { useUserStore } from "@/stores/user";

Expand Down Expand Up @@ -67,6 +67,14 @@
);
}

export function download_admin_result(sample_id: number, sample_name: string) {
download_file_from_endpoint(
"admin/result",
{ sample_id: sample_id },
`${sample_name}_admin.zip`,
);
}

export function logout() {
const user = useUserStore();
user.user = null;
Expand Down
85 changes: 51 additions & 34 deletions frontend/src/views/AdminView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import UsersTable from "@/components/UsersTable.vue";
import ListComponent from "@/components/ListComponent.vue";
import JobsTable from "@/components/JobsTable.vue";
import ListItem from "@/components/ListItem.vue";
import { FwbButton } from "flowbite-vue";
import { FwbButton, FwbTab, FwbTabs } from "flowbite-vue";
import { ref } from "vue";
import type { Sample } from "@/utils/types";
import { apiClient, logout } from "@/utils/api-client";
Expand All @@ -26,6 +26,7 @@ function generate_api_token() {
});
}

const activeTab = ref("samples");
const samples = ref([] as Sample[]);

function get_samples() {
Expand All @@ -48,39 +49,55 @@ get_samples();
<template>
<main>
<div class="p-4">
<ListComponent>
<ListItem title="Settings">
<SettingsTable />
</ListItem>
<ListItem title="Generate runner API Token">
<p>
Here you can generate a new runner user with an API token for
authentication. Note the token should be kept secret! It is valid
for 6 months, then you will need to generate a new one:
</p>
<p>
<fwb-button @click="generate_api_token">
Generate API Token and copy to clipboard
</fwb-button>
</p>
</ListItem>
<ListItem title="Runners">
<UsersTable :is_runner="true"></UsersTable>
</ListItem>
<ListItem title="Users">
<UsersTable :is_runner="false"></UsersTable>
</ListItem>
<ListItem title="Samples">
<SamplesTable
:samples="samples"
:admin="true"
@samples-modified="get_samples"
></SamplesTable>
</ListItem>
<ListItem title="Runner Jobs">
<JobsTable />
</ListItem>
</ListComponent>
<fwb-tabs v-model="activeTab" variant="underline" class="p-5">
<fwb-tab name="samples" title="Samples">
<ListComponent>
<ListItem title="Samples">
<SamplesTable
:samples="samples"
:admin="true"
@samples-modified="get_samples"
></SamplesTable>
</ListItem>
</ListComponent>
</fwb-tab>
<fwb-tab name="users" title="Users">
<ListComponent>
<ListItem title="Users">
<UsersTable :is_runner="false"></UsersTable>
</ListItem>
</ListComponent>
</fwb-tab>
<fwb-tab name="runners" title="Runners">
<ListComponent>
<ListItem title="Generate runner API Token">
<p>
Here you can generate a new runner user with an API token for
authentication. Note the token should be kept secret! It is
valid for 6 months, then you will need to generate a new one:
</p>
<p>
<fwb-button @click="generate_api_token">
Generate API Token and copy to clipboard
</fwb-button>
</p>
</ListItem>
<ListItem title="Runners">
<UsersTable :is_runner="true"></UsersTable>
</ListItem>
<ListItem title="Runner Jobs">
<JobsTable />
</ListItem>
</ListComponent>
</fwb-tab>
<fwb-tab name="settings" title="Settings">
<ListComponent>
<ListItem title="Settings">
<SettingsTable />
</ListItem>
</ListComponent>
</fwb-tab>
</fwb-tabs>
</div>
</main>
</template>
Loading
Loading