aboutsummaryrefslogtreecommitdiff
path: root/uploader
diff options
context:
space:
mode:
Diffstat (limited to 'uploader')
-rw-r--r--uploader/files/functions.py11
-rw-r--r--uploader/files/views.py43
-rw-r--r--uploader/oauth2/client.py3
-rw-r--r--uploader/phenotypes/views.py253
-rw-r--r--uploader/static/css/styles.css14
-rw-r--r--uploader/templates/base.html2
-rw-r--r--uploader/templates/phenotypes/add-phenotypes-raw-files.html107
-rw-r--r--uploader/templates/phenotypes/job-status.html26
-rw-r--r--uploader/templates/phenotypes/macro-display-preview-table.html4
-rw-r--r--uploader/templates/phenotypes/macro-display-resumable-elements.html60
-rw-r--r--uploader/templates/phenotypes/review-job-data.html101
-rw-r--r--uploader/templates/phenotypes/view-dataset.html3
-rw-r--r--uploader/templates/phenotypes/view-phenotype.html89
13 files changed, 573 insertions, 143 deletions
diff --git a/uploader/files/functions.py b/uploader/files/functions.py
index 5a3dece..7b9f06b 100644
--- a/uploader/files/functions.py
+++ b/uploader/files/functions.py
@@ -10,12 +10,15 @@ from werkzeug.datastructures import FileStorage
from .chunks import chunked_binary_read
-def save_file(fileobj: FileStorage, upload_dir: Path) -> Path:
+def save_file(fileobj: FileStorage, upload_dir: Path, hashed: bool = True) -> Path:
"""Save the uploaded file and return the path."""
assert bool(fileobj), "Invalid file object!"
- hashed_name = hashlib.sha512(
- f"{fileobj.filename}::{datetime.now().isoformat()}".encode("utf8")
- ).hexdigest()
+ hashed_name = (
+ hashlib.sha512(
+ f"{fileobj.filename}::{datetime.now().isoformat()}".encode("utf8")
+ ).hexdigest()
+ if hashed else
+ fileobj.filename)
filename = Path(secure_filename(hashed_name)) # type: ignore[arg-type]
if not upload_dir.exists():
upload_dir.mkdir()
diff --git a/uploader/files/views.py b/uploader/files/views.py
index cd5f00f..8d81654 100644
--- a/uploader/files/views.py
+++ b/uploader/files/views.py
@@ -8,6 +8,11 @@ from .chunks import chunk_name, chunks_directory
files = Blueprint("files", __name__)
+def target_file(fileid: str) -> Path:
+ """Compute the full path for the target file."""
+ return Path(app.config["UPLOAD_FOLDER"], fileid)
+
+
@files.route("/upload/resumable", methods=["GET"])
def resumable_upload_get():
"""Used for checking whether **ALL** chunks have been uploaded."""
@@ -21,9 +26,24 @@ def resumable_upload_get():
"statuscode": 400
}), 400
+ # If the complete target file exists, return 200 for all chunks.
+ _targetfile = target_file(fileid)
+ if _targetfile.exists():
+ return jsonify({
+ "uploaded-file": _targetfile.name,
+ "original-name": filename,
+ "chunk": chunk,
+ "message": "The complete file already exists.",
+ "statuscode": 200
+ }), 200
+
if Path(chunks_directory(fileid),
chunk_name(filename, chunk)).exists():
- return "OK"
+ return jsonify({
+ "chunk": chunk,
+ "message": f"Chunk {chunk} exists.",
+ "statuscode": 200
+ }), 200
return jsonify({
"message": f"Chunk {chunk} was not found.",
@@ -52,16 +72,15 @@ def resumable_upload_post():
"resumableFilename", default="", type=str) or ""
_fileid = request.form.get(
"resumableIdentifier", default="", type=str) or ""
- _targetfile = Path(app.config["UPLOAD_FOLDER"], _fileid)
+ _targetfile = target_file(_fileid)
if _targetfile.exists():
return jsonify({
- "message": (
- "A file with a similar unique identifier has previously been "
- "uploaded and possibly is/has being/been processed."),
- "error": "BadRequest",
- "statuscode": 400
- }), 400
+ "uploaded-file": _targetfile.name,
+ "original-name": _uploadfilename,
+ "message": "File was uploaded successfully!",
+ "statuscode": 200
+ }), 200
try:
chunks_directory(_fileid).mkdir(exist_ok=True, parents=True)
@@ -78,14 +97,14 @@ def resumable_upload_post():
chunks_directory(_fileid).rmdir()
return jsonify({
"uploaded-file": _targetfile.name,
+ "original-name": _uploadfilename,
"message": "File was uploaded successfully!",
"statuscode": 200
}), 200
return jsonify({
- "message": "Some chunks were not uploaded!",
- "error": "ChunksUploadError",
- "error-description": "Some chunks were not uploaded!"
- })
+ "message": f"Chunk {int(_chunk)} uploaded successfully.",
+ "statuscode": 201
+ }), 201
except Exception as exc:# pylint: disable=[broad-except]
msg = "Error processing uploaded file chunks."
app.logger.error(msg, exc_info=True, stack_info=True)
diff --git a/uploader/oauth2/client.py b/uploader/oauth2/client.py
index e7128de..1efa299 100644
--- a/uploader/oauth2/client.py
+++ b/uploader/oauth2/client.py
@@ -112,7 +112,8 @@ def oauth2_client():
try:
jwt = JsonWebToken(["RS256"]).decode(
token["access_token"], key=jwk)
- return datetime.now().timestamp() > jwt["exp"]
+ if bool(jwt.get("exp")):
+ return datetime.now().timestamp() > jwt["exp"]
except BadSignatureError as _bse:
pass
diff --git a/uploader/phenotypes/views.py b/uploader/phenotypes/views.py
index f10ba09..8ecd305 100644
--- a/uploader/phenotypes/views.py
+++ b/uploader/phenotypes/views.py
@@ -4,7 +4,8 @@ import uuid
import json
import datetime
from pathlib import Path
-from functools import wraps
+from zipfile import ZipFile
+from functools import wraps, reduce
from logging import INFO, ERROR, DEBUG, FATAL, CRITICAL, WARNING
from redis import Redis
@@ -14,6 +15,7 @@ from gn_libs.mysqldb import database_connection
from flask import (flash,
request,
url_for,
+ jsonify,
redirect,
Blueprint,
current_app as app)
@@ -42,7 +44,8 @@ from .models import (dataset_by_id,
phenotypes_count,
save_new_dataset,
dataset_phenotypes,
- datasets_by_population)
+ datasets_by_population,
+ phenotype_publication_data)
phenotypesbp = Blueprint("phenotypes", __name__)
render_template = make_template_renderer("phenotypes")
@@ -218,16 +221,26 @@ def view_phenotype(# pylint: disable=[unused-argument]
):
"""View an individual phenotype from the dataset."""
def __render__(privileges):
+ phenotype = phenotype_by_id(conn,
+ species["SpeciesId"],
+ population["Id"],
+ dataset["Id"],
+ xref_id)
return render_template(
"phenotypes/view-phenotype.html",
species=species,
population=population,
dataset=dataset,
- phenotype=phenotype_by_id(conn,
- species["SpeciesId"],
- population["Id"],
- dataset["Id"],
- xref_id),
+ phenotype=phenotype,
+ has_se=all(bool(item.get("error")) for item in phenotype["data"]),
+ publish_data={
+ key.replace("_", " "): val
+ for key,val in
+ (phenotype_publication_data(conn, phenotype["Id"]) or {}).items()
+ if (key in ("PubMed_ID", "Authors", "Title", "Journal")
+ and val is not None
+ and val.strip() is not "")
+ },
privileges=(privileges
### For demo! Do not commit this part
+ ("group:resource:edit-resource",
@@ -307,8 +320,7 @@ def create_dataset(species: dict, population: dict, **kwargs):# pylint: disable=
population_id=population["Id"]))
-def process_phenotypes_rqtl2_bundle(
- rconn: Redis, species: dict, population: dict, dataset: dict):
+def process_phenotypes_rqtl2_bundle(error_uri):
"""Process phenotypes from the uploaded R/qtl2 bundle."""
_redisuri = app.config["REDIS_URL"]
_sqluri = app.config["SQL_URI"]
@@ -317,64 +329,59 @@ def process_phenotypes_rqtl2_bundle(
phenobundle = save_file(request.files["phenotypes-bundle"],
Path(app.config["UPLOAD_FOLDER"]))
rqc.validate_bundle(phenobundle)
+ return phenobundle
except AssertionError as _aerr:
app.logger.debug("File upload error!", exc_info=True)
flash("Expected a zipped bundle of files with phenotypes' "
"information.",
"alert-danger")
- return add_phenos_uri
+ return error_uri
except rqe.RQTLError as rqtlerr:
app.logger.debug("Bundle validation error!", exc_info=True)
flash("R/qtl2 Error: " + " ".join(rqtlerr.args), "alert-danger")
- return add_phenos_uri
-
- _jobid = uuid.uuid4()
- _namespace = jobs.jobsnamespace()
- _ttl_seconds = app.config["JOBS_TTL_SECONDS"]
- _job = jobs.launch_job(
- jobs.initialise_job(
- rconn,
- _namespace,
- str(_jobid),
- [sys.executable, "-m", "scripts.rqtl2.phenotypes_qc", _sqluri,
- _redisuri, _namespace, str(_jobid), str(species["SpeciesId"]),
- str(population["Id"]),
- # str(dataset["Id"]),
- str(phenobundle),
- "--loglevel",
- {
- INFO: "INFO",
- ERROR: "ERROR",
- DEBUG: "DEBUG",
- FATAL: "FATAL",
- CRITICAL: "CRITICAL",
- WARNING: "WARNING"
- }[app.logger.getEffectiveLevel()],
- "--redisexpiry",
- str(_ttl_seconds)], "phenotype_qc", _ttl_seconds,
- {"job-metadata": json.dumps({
- "speciesid": species["SpeciesId"],
- "populationid": population["Id"],
- "datasetid": dataset["Id"],
- "bundle": str(phenobundle.absolute())})}),
- _redisuri,
- f"{app.config['UPLOAD_FOLDER']}/job_errors")
-
- app.logger.debug("JOB DETAILS: %s", _job)
-
- return redirect(url_for("species.populations.phenotypes.job_status",
- species_id=species["SpeciesId"],
- population_id=population["Id"],
- dataset_id=dataset["Id"],
- job_id=str(_job["jobid"])))
-
-
-def process_phenotypes_individual_files(rconn, species, population, dataset):
+ return error_uri
+
+
+def process_phenotypes_individual_files(error_uri):
"""Process the uploaded individual files."""
- ## Handle huge file uploads here...
- ## Convert files and settings to R/qtl2 bundle
- ## Use same processing as R/qtl2 bundle (after some refactoring)
- raise NotImplementedError("Implement this!")
+ form = request.form
+ cdata = {
+ "sep": form["file-separator"],
+ "comment.char": form["file-comment-character"],
+ "na.strings": form["file-na"].split(" "),
+ }
+ bundlepath = Path(app.config["UPLOAD_FOLDER"],
+ f"{str(uuid.uuid4()).replace('-', '')}.zip")
+ with ZipFile(bundlepath,mode="w") as zfile:
+ for rqtlkey, formkey in (("phenocovar", "phenotype-descriptions"),
+ ("pheno", "phenotype-data"),
+ ("phenose", "phenotype-se"),
+ ("phenonum", "phenotype-n")):
+ if form.get("resumable-upload", False):
+ # Chunked upload of large files was used
+ filedata = json.loads(form[formkey])
+ zfile.write(
+ Path(app.config["UPLOAD_FOLDER"], filedata["uploaded-file"]),
+ arcname=filedata["original-name"])
+ cdata[rqtlkey] = cdata.get(rqtlkey, []) + [filedata["original-name"]]
+ else:
+ # TODO: Check this path: fix any bugs.
+ _sentfile = request.files[formkey]
+ if not bool(_sentfile):
+ flash(f"Expected file ('{formkey}') was not provided.",
+ "alert-danger")
+ return error_uri
+
+ filepath = save_file(
+ _sentfile, Path(app.config["UPLOAD_FOLDER"]), hashed=False)
+ zfile.write(
+ Path(app.config["UPLOAD_FOLDER"], filepath),
+ arcname=filepath.name)
+ cdata[rqtlkey] = cdata.get(rqtlkey, []) + [filepath.name]
+
+ zfile.writestr("control_data.json", data=json.dumps(cdata, indent=2))
+
+ return bundlepath
@phenotypesbp.route(
@@ -420,11 +427,57 @@ def add_phenotypes(species: dict, population: dict, dataset: dict, **kwargs):# p
use_bundle=use_bundle,
activelink="add-phenotypes")
- if use_bundle:
- return process_phenotypes_rqtl2_bundle(
- rconn, species, population, dataset)
- return process_phenotypes_individual_files(
- rconn, species, population, dataset)
+ phenobundle = (process_phenotypes_rqtl2_bundle(add_phenos_uri)
+ if use_bundle else
+ process_phenotypes_individual_files(add_phenos_uri))
+
+ _jobid = uuid.uuid4()
+ _namespace = jobs.jobsnamespace()
+ _ttl_seconds = app.config["JOBS_TTL_SECONDS"]
+ _job = jobs.launch_job(
+ jobs.initialise_job(
+ rconn,
+ _namespace,
+ str(_jobid),
+ [sys.executable, "-m", "scripts.rqtl2.phenotypes_qc", _sqluri,
+ _redisuri, _namespace, str(_jobid), str(species["SpeciesId"]),
+ str(population["Id"]),
+ # str(dataset["Id"]),
+ str(phenobundle),
+ "--loglevel",
+ {
+ INFO: "INFO",
+ ERROR: "ERROR",
+ DEBUG: "DEBUG",
+ FATAL: "FATAL",
+ CRITICAL: "CRITICAL",
+ WARNING: "WARNING"
+ }[app.logger.getEffectiveLevel()],
+ "--redisexpiry",
+ str(_ttl_seconds)], "phenotype_qc", _ttl_seconds,
+ {"job-metadata": json.dumps({
+ "speciesid": species["SpeciesId"],
+ "populationid": population["Id"],
+ "datasetid": dataset["Id"],
+ "bundle": str(phenobundle.absolute())})}),
+ _redisuri,
+ f"{app.config['UPLOAD_FOLDER']}/job_errors")
+
+ app.logger.debug("JOB DETAILS: %s", _job)
+ jobstatusuri = url_for("species.populations.phenotypes.job_status",
+ species_id=species["SpeciesId"],
+ population_id=population["Id"],
+ dataset_id=dataset["Id"],
+ job_id=str(_job["jobid"]))
+ return ((jsonify({
+ "redirect-to": jobstatusuri,
+ "statuscode": 200,
+ "message": ("Follow the 'redirect-to' URI to see the state "
+ "of the quality-control job started for your "
+ "uploaded files.")
+ }), 200)
+ if request.form.get("resumable-upload", False) else
+ redirect(jobstatusuri))
@phenotypesbp.route(
@@ -460,3 +513,77 @@ def job_status(
metadata=jobs.job_files_metadata(
rconn, jobs.jobsnamespace(), job['jobid']),
activelink="add-phenotypes")
+
+
+@phenotypesbp.route(
+ "<int:species_id>/populations/<int:population_id>/phenotypes/datasets"
+ "/<int:dataset_id>/review-job/<uuid:job_id>",
+ methods=["GET"])
+@require_login
+@with_dataset(
+ species_redirect_uri="species.populations.phenotypes.index",
+ population_redirect_uri="species.populations.phenotypes.select_population",
+ redirect_uri="species.populations.phenotypes.list_datasets")
+def review_job_data(
+ species: dict,
+ population: dict,
+ dataset: dict,
+ job_id: uuid.UUID,
+ **kwargs
+):# pylint: disable=[unused-argument]
+ """Review data one more time before entering it into the database."""
+ with Redis.from_url(app.config["REDIS_URL"], decode_responses=True) as rconn:
+ try:
+ job = jobs.job(rconn, jobs.jobsnamespace(), str(job_id))
+ except jobs.JobNotFound as _jnf:
+ job = None
+
+ def __metadata_by_type__(by_type, item):
+ filetype = item[1]["filetype"]
+ return {
+ **by_type,
+ filetype: (by_type.get(filetype, tuple())
+ + ({"filename": item[0], **item[1]},))
+ }
+ metadata = reduce(__metadata_by_type__,
+ (jobs.job_files_metadata(
+ rconn, jobs.jobsnamespace(), job['jobid'])
+ if job else {}).items(),
+ {})
+
+ def __desc__(filetype):
+ match filetype:
+ case "phenocovar":
+ desc = "phenotypes"
+ case "pheno":
+ desc = "phenotypes data"
+ case "phenose":
+ desc = "phenotypes standard-errors"
+ case "phenonum":
+ desc = "phenotypes samples"
+ case _:
+ desc = f"unknown file type '{filetype}'."
+
+ return desc
+
+ def __summarise__(filetype, files):
+ return {
+ "filetype": filetype,
+ "number-of-files": len(files),
+ "total-data-rows": sum(
+ int(afile["linecount"]) - 1 for afile in files),
+ "description": __desc__(filetype)
+ }
+
+ summary = {
+ filetype: __summarise__(filetype, meta)
+ for filetype,meta in metadata.items()
+ }
+ return render_template("phenotypes/review-job-data.html",
+ species=species,
+ population=population,
+ dataset=dataset,
+ job_id=job_id,
+ job=job,
+ summary=summary,
+ activelink="add-phenotypes")
diff --git a/uploader/static/css/styles.css b/uploader/static/css/styles.css
index f482c1b..1c3e677 100644
--- a/uploader/static/css/styles.css
+++ b/uploader/static/css/styles.css
@@ -3,7 +3,7 @@ body {
box-sizing: border-box;
display: grid;
grid-template-columns: 1fr 6fr;
- grid-template-rows: 5em 100%;
+ grid-template-rows: 4em 100%;
grid-gap: 20px;
font-family: Georgia, Garamond, serif;
@@ -24,7 +24,7 @@ body {
}
#header .header {
- font-size: 2em;
+ font-size: 1.7em;
display: inline-block;
text-align: start;
}
@@ -38,7 +38,7 @@ body {
border-width: 1px;
border-color: #FFFFFF;
vertical-align: middle;
- margin: 0.2em;
+ margin: 0.01em;
border-style: solid;
border-width: 2px;
border-radius: 0.5em;
@@ -66,7 +66,8 @@ body {
}
.pagetitle {
- padding-top: 0.5em;
+ line-height: 1;
+ padding-top: 0.2em;
/* background: pink; */
border-radius: 0.5em;
/* background-color: #6699CC; */
@@ -74,10 +75,11 @@ body {
background-color: #88BBEE;
}
-.pagetitle h1 {
+.pagetitle .title {
text-align: start;
text-transform: capitalize;
- padding-left: 0.25em;
+ padding-left: 0.5em;
+ font-size: 1.7em;
}
.pagetitle .breadcrumb {
diff --git a/uploader/templates/base.html b/uploader/templates/base.html
index 3a8ef16..c124b13 100644
--- a/uploader/templates/base.html
+++ b/uploader/templates/base.html
@@ -93,7 +93,7 @@
<main id="main" class="main container-fluid">
<div class="pagetitle row">
- <h1>GN Uploader: {%block pagetitle%}{%endblock%}</h1>
+ <span class="title">GN Uploader: {%block pagetitle%}{%endblock%}</span>
<nav>
<ol class="breadcrumb">
<li {%if activelink is not defined or activelink=="home"%}
diff --git a/uploader/templates/phenotypes/add-phenotypes-raw-files.html b/uploader/templates/phenotypes/add-phenotypes-raw-files.html
index 2264b59..d9a8424 100644
--- a/uploader/templates/phenotypes/add-phenotypes-raw-files.html
+++ b/uploader/templates/phenotypes/add-phenotypes-raw-files.html
@@ -502,6 +502,7 @@
var resumableDisplayFiles = (display_area, files) => {
files.forEach((file) => {
+ display_area.find(".file-display").remove();
var display_element = display_area
.find(".file-display-template")
.clone();
@@ -563,20 +564,58 @@
};
};
+ var processForm = (form) => {
+ var formdata = new FormData(form);
+ uploaded_files.forEach((msg) => {
+ formdata.delete(msg["file-input-name"]);
+ formdata.append(msg["file-input-name"], JSON.stringify({
+ "uploaded-file": msg["uploaded-file"],
+ "original-name": msg["original-name"]
+ }));
+ });
+ formdata.append("resumable-upload", "true");
+ return formdata;
+ }
+
+ var uploaded_files = new Set();
+ var submitForm = (new_file) => {
+ uploaded_files.add(new_file);
+ if(uploaded_files.size === resumables.length) {
+ var form = $("#frm-add-phenotypes");
+ if(form.length !== 1) {
+ // TODO: Handle error somehow?
+ alert("Could not find form!!!");
+ return false;
+ }
- var uploadSuccess = () => {
+ $.ajax({
+ "url": form.attr("action"),
+ "type": "POST",
+ "data": processForm(form[0]),
+ "processData": false,
+ "contentType": false,
+ "success": (data, textstatus, jqxhr) => {
+ // TODO: Redirect to endpoint that should come as part of the
+ // success/error message.
+ console.log("SUCCESS DATA: ", data);
+ console.log("SUCCESS STATUS: ", textstatus);
+ console.log("SUCCESS jqXHR: ", jqxhr);
+ },
+ });
+ return false;
+ }
+ };
+
+ var uploadSuccess = (file_input_name) => {
return (file, message) => {
- console.log("THE FILE:", file);
- console.log("THE SUCCESS MESSAGE:", message);
- // TODOS:
- // * Save filename/filepath somewhere
- // * Trigger some function that will run when all files have succeeded
+ submitForm({...JSON.parse(message), "file-input-name": file_input_name});
};
};
var uploadError = () => {
return (message, file) => {
+ $("#frm-add-phenotypes input[type=submit]").removeAttr("disabled");
console.log("THE FILE:", file);
console.log("THE ERROR MESSAGE:", message);
};
@@ -588,6 +627,9 @@
var the_form = $("#" + form_id);
var file_input = $("#" + file_input_id);
var submit_button = the_form.find("input[type=submit]");
+ if(file_input.length != 1) {
+ return false;
+ }
var r = errorHandler(
fileSuccessHandler(
uploadStartHandler(
@@ -617,7 +659,7 @@
startUpload($("#" + resumable_element_id + "-browse-button"),
$("#" + resumable_element_id + "-retry-button"),
$("#" + resumable_element_id + "-cancel-button"))),
- uploadSuccess()),
+ uploadSuccess(file_input.attr("name"))),
uploadError());
/** Setup progress indicator **/
@@ -628,12 +670,61 @@
return r;
};
+ var resumables = [
+ ["frm-add-phenotypes", "finput-phenotype-descriptions", "resumable-phenotype-descriptions", "tbl-preview-pheno-desc"],
+ ["frm-add-phenotypes", "finput-phenotype-data", "resumable-phenotype-data", "tbl-preview-pheno-data"],
+ ["frm-add-phenotypes", "finput-phenotype-se", "resumable-phenotype-se", "tbl-preview-pheno-se"],
+ ["frm-add-phenotypes", "finput-phenotype-n", "resumable-phenotype-n", "tbl-preview-pheno-n"],
+ ].map((row) => {
+ return makeResumableObject(row[0], row[1], row[2], row[3]);
+ }).filter((val) => {
+ return Boolean(val);
+ });
+
$("#frm-add-phenotypes input[type=submit]").on("click", (event) => {
event.preventDefault();
// TODO: Check all the relevant files exist
+ // TODO: Verify that files are not duplicated
+ var filenames = [];
+ var nondupfiles = [];
+ resumables.forEach((r) => {
+ var fname = r.files[0].file.name;
+ filenames.push(fname);
+ if(!nondupfiles.includes(fname)) {
+ nondupfiles.push(fname);
+ }
+ });
+
+ // Check that all files were provided
+ if(resumables.length !== filenames.length) {
+ window.alert("You MUST provide all the files requested.");
+ event.target.removeAttribute("disabled");
+ return false;
+ }
+
+ // Check that there are no duplicate files
+ var duplicates = Object.entries(filenames.reduce(
+ (acc, curr, idx, arr) => {
+ acc[curr] = (acc[curr] || 0) + 1;
+ return acc;
+ },
+ {})).filter((entry) => {return entry[1] !== 1;});
+ if(duplicates.length > 0) {
+ var msg = "The file(s):\r\n";
+ msg = msg + duplicates.reduce(
+ (msgstr, afile) => {
+ return msgstr + " • " + afile[0] + "\r\n";
+ },
+ "");
+ msg = msg + "is(are) duplicated. Please fix and try again.";
+ window.alert(msg);
+ event.target.removeAttribute("disabled");
+ return false;
+ }
// TODO: Check all fields
// Start the uploads.
- r.upload();
+ event.target.setAttribute("disabled", "disabled");
+ resumables.forEach((r) => {r.upload();});
});
</script>
{%endblock%}
diff --git a/uploader/templates/phenotypes/job-status.html b/uploader/templates/phenotypes/job-status.html
index 5f13876..12963c1 100644
--- a/uploader/templates/phenotypes/job-status.html
+++ b/uploader/templates/phenotypes/job-status.html
@@ -31,10 +31,10 @@
{%if job%}
<h4 class="subheading">Progress</h4>
-<div class="row">
+<div class="row" style="overflow:scroll;">
<p><strong>Process Status:</strong> {{job.status}}</p>
{%if metadata%}
- <table class="table">
+ <table class="table table-responsive">
<thead>
<tr>
<th>File</th>
@@ -56,32 +56,39 @@
</tbody>
</table>
{%endif%}
+</div>
+
+<div class="row">
{%if job.status in ("completed:success", "success")%}
<p>
{%if errors | length == 0%}
- <a href="#"
- class="not-implemented btn btn-primary"
+ <a href="{{url_for('species.populations.phenotypes.review_job_data',
+ species_id=species.SpeciesId,
+ population_id=population.Id,
+ dataset_id=dataset.Id,
+ job_id=job_id)}}"
+ class="btn btn-primary"
title="Continue to process data">Continue</a>
{%else%}
<span class="text-muted"
- disabled="disabled"
- style="border: solid 2px;border-radius: 5px;padding: 0.3em;">
+ disabled="disabled"
+ style="border: solid 2px;border-radius: 5px;padding: 0.3em;">
Cannot continue due to errors. Please fix the errors first.
- </a>
+ </span>
{%endif%}
</p>
{%endif%}
</div>
<h4 class="subheading">Errors</h4>
-<div class="row" style="max-height: 20em; overflow: auto;">
+<div class="row" style="max-height: 20em; overflow: scroll;">
{%if errors | length == 0 %}
<p class="text-info">
<span class="glyphicon glyphicon-info-sign"></span>
No errors found so far
</p>
{%else%}
- <table class="table">
+ <table class="table table-responsive">
<thead style="position: sticky; top: 0; background: white;">
<tr>
<th>File</th>
@@ -89,6 +96,7 @@
<th>Column</th>
<th>Value</th>
<th>Message</th>
+ </tr>
</thead>
<tbody style="font-size: 0.9em;">
diff --git a/uploader/templates/phenotypes/macro-display-preview-table.html b/uploader/templates/phenotypes/macro-display-preview-table.html
index 7509158..f54c53e 100644
--- a/uploader/templates/phenotypes/macro-display-preview-table.html
+++ b/uploader/templates/phenotypes/macro-display-preview-table.html
@@ -2,8 +2,8 @@
<div class="card" style="max-width: 676px;">
<div class="card-body">
<h5 class="card-title">Phenotypes '{{filetype | title}}' File Preview</h5>
- <div class="card-text">
- <table id="{{tableid}}" class="table table-condensed table-responsive" style="overflow: hidden;">
+ <div class="card-text" style="overflow: scroll;">
+ <table id="{{tableid}}" class="table table-condensed table-responsive">
<thead>
<tr>
</tr>
diff --git a/uploader/templates/phenotypes/macro-display-resumable-elements.html b/uploader/templates/phenotypes/macro-display-resumable-elements.html
new file mode 100644
index 0000000..b0bf1b5
--- /dev/null
+++ b/uploader/templates/phenotypes/macro-display-resumable-elements.html
@@ -0,0 +1,60 @@
+{%macro display_resumable_elements(id, title, help)%}
+<div id="{{id}}"
+ class="resumable-elements hidden"
+ style="background:#D4D4EE;border-radius: 5px;;padding: 1em;border-left: solid #B2B2CC 1px;border-bottom: solid #B2B2CC 2px;margin-top:0.3em;">
+ <strong style="line-height: 1.2em;">{{title | title}}</strong>
+
+ <span class="form-text text-muted">{{help | safe}}</span>
+
+ <div id="{{id}}-selected-files"
+ class="resumable-selected-files"
+ style="display:flex;flex-direction:row;flex-wrap: wrap;justify-content:space-around;gap:10px 20px;">
+ <div class="panel panel-info file-display-template hidden">
+ <div class="panel-heading filename">The Filename Goes Here!</div>
+ <div class="panel-body">
+ <ul>
+ <li>
+ <strong>Name</strong>:
+ <span class="filename">the file's name</span></li>
+
+ <li><strong>Size</strong>: <span class="filesize">0 MB</span></li>
+
+ <li>
+ <strong>Unique Identifier</strong>:
+ <span class="fileuniqueid">brrr</span></li>
+
+ <li>
+ <strong>Mime</strong>:
+ <span class="filemimetype">text/csv</span></li>
+ </ul>
+ </div>
+ </div>
+ </div>
+
+ <a id="{{id}}-browse-button"
+ class="resumable-browse-button btn btn-info"
+ href="#"
+ style="margin-left: 80%;">Browse</a>
+
+ <div id="{{id}}-progress-bar" class="progress hidden">
+ <div class="progress-bar"
+ role="progress-bar"
+ aria-valuenow="60"
+ aria-valuemin="0"
+ aria-valuemax="100"
+ style="width: 0%;">
+ Uploading: 60%
+ </div>
+ </div>
+
+ <div id="{{id}}-cancel-resume-buttons">
+ <a id="{{id}}-resume-button"
+ class="resumable-resume-button btn btn-info hidden"
+ href="#">resume upload</a>
+
+ <a id="{{id}}-cancel-button"
+ class="resumable-cancel-button btn btn-danger hidden"
+ href="#">cancel upload</a>
+ </div>
+</div>
+{%endmacro%}
diff --git a/uploader/templates/phenotypes/review-job-data.html b/uploader/templates/phenotypes/review-job-data.html
new file mode 100644
index 0000000..7bc8c62
--- /dev/null
+++ b/uploader/templates/phenotypes/review-job-data.html
@@ -0,0 +1,101 @@
+{%extends "phenotypes/base.html"%}
+{%from "cli-output.html" import cli_output%}
+{%from "flash_messages.html" import flash_all_messages%}
+{%from "macro-table-pagination.html" import table_pagination%}
+{%from "phenotypes/macro-display-pheno-dataset-card.html" import display_pheno_dataset_card%}
+
+{%block extrameta%}
+{%if not job%}
+<meta http-equiv="refresh"
+ content="20; url={{url_for('species.populations.phenotypes.view_dataset', species_id=species.SpeciesId,
+ population_id=population.Id,
+ dataset_id=dataset.Id)}}" />
+{%endif%}
+{%endblock%}
+
+{%block title%}Phenotypes{%endblock%}
+
+{%block pagetitle%}Phenotypes{%endblock%}
+
+{%block lvl4_breadcrumbs%}
+<li {%if activelink=="add-phenotypes"%}
+ class="breadcrumb-item active"
+ {%else%}
+ class="breadcrumb-item"
+ {%endif%}>
+ <a href="{{url_for('species.populations.phenotypes.add_phenotypes',
+ species_id=species.SpeciesId,
+ population_id=population.Id,
+ dataset_id=dataset.Id)}}">View Datasets</a>
+</li>
+{%endblock%}
+
+{%block contents%}
+
+{%if job%}
+<div class="row">
+ <h3 class="heading">Data Review</h3>
+ <p>The &#x201C;<strong>{{dataset.FullName}}</strong>&#x201D; dataset from the
+ &#x201C;<strong>{{population.FullName}}</strong>&#x201D; population of the
+ species &#x201C;<strong>{{species.SpeciesName}} ({{species.FullName}})</strong>&#x201D;
+ will be updated as follows:</p>
+
+ {%for ftype in ("phenocovar", "pheno", "phenose", "phenonum")%}
+ {%if summary.get(ftype, False)%}
+ <ul>
+ <li>A total of {{summary[ftype]["number-of-files"]}} files will be processed
+ adding {%if ftype == "phenocovar"%}(possibly){%endif%}
+ {{summary[ftype]["total-data-rows"]}} new
+ {%if ftype == "phenocovar"%}
+ phenotypes
+ {%else%}
+ {{summary[ftype]["description"]}} rows
+ {%endif%}
+ to the database.
+ </li>
+ </ul>
+ {%endif%}
+ {%endfor%}
+
+ <a href="#" class="not-implemented btn btn-primary">continue</a>
+</div>
+{%else%}
+<div class="row">
+ <h4 class="subheading">Invalid Job</h3>
+ <p class="text-danger">
+ Could not find a job with the ID: <strong>{{job_id}}.</p>
+ <p>You will be redirected in
+ <span id="countdown-element" class="text-info">20</span> second(s)</p>
+ <p class="text-muted">
+ <small>
+ If you are not redirected, please
+ <a href="{{url_for(
+ 'species.populations.phenotypes.view_dataset',
+ species_id=species.SpeciesId,
+ population_id=population.Id,
+ dataset_id=dataset.Id)}}">click here</a> to continue
+ </small>
+ </p>
+</div>
+{%endif%}
+{%endblock%}
+
+{%block sidebarcontents%}
+{{display_pheno_dataset_card(species, population, dataset)}}
+{%endblock%}
+
+
+{%block javascript%}
+<script type="text/javascript">
+ $(document).ready(function() {
+ var countdown = 20;
+ var countdown_element = $("#countdown-element");
+ if(countdown_element.length === 1) {
+ intv = window.setInterval(function() {
+ countdown = countdown - 1;
+ countdown_element.html(countdown);
+ }, 1000);
+ }
+ });
+</script>
+{%endblock%}
diff --git a/uploader/templates/phenotypes/view-dataset.html b/uploader/templates/phenotypes/view-dataset.html
index 66de5d8..011f8f6 100644
--- a/uploader/templates/phenotypes/view-dataset.html
+++ b/uploader/templates/phenotypes/view-dataset.html
@@ -79,7 +79,8 @@
population_id=population.Id,
dataset_id=dataset.Id,
xref_id=pheno['pxr.Id'])}}"
- title="View phenotype details">
+ title="View phenotype details"
+ target="_blank">
{{pheno.InbredSetCode}}_{{pheno["pxr.Id"]}}</a></td>
<td>{{pheno.Post_publication_description or pheno.Pre_publication_abbreviation or pheno.Original_description}}</td>
</tr>
diff --git a/uploader/templates/phenotypes/view-phenotype.html b/uploader/templates/phenotypes/view-phenotype.html
index 99bb8e5..18ac202 100644
--- a/uploader/templates/phenotypes/view-phenotype.html
+++ b/uploader/templates/phenotypes/view-phenotype.html
@@ -34,51 +34,66 @@
<td>{{phenotype.Post_publication_description or phenotype.Pre_publication_abbreviation or phenotype.Original_description}}
</tr>
<tr>
- <td><strong>Cross-Reference ID</strong></td>
- <td>{{phenotype.xref_id}}</td>
- </tr>
- <tr>
- <td><strong>Collation</strong></td>
+ <td><strong>Database</strong></td>
<td>{{dataset.FullName}}</td>
</tr>
<tr>
<td><strong>Units</strong></td>
<td>{{phenotype.Units}}</td>
</tr>
+ {%for key,value in publish_data.items()%}
+ <tr>
+ <td><strong>{{key}}</strong></td>
+ <td>{{value}}</td>
+ </tr>
+ {%else%}
+ <tr>
+ <td colspan="2" class="text-muted">
+ <span class="glyphicon glyphicon-exclamation-sign"></span>
+ No publication data found.
+ </td>
+ </tr>
+ {%endfor%}
</tbody>
</table>
+ </div>
+</div>
- <form action="#edit-delete-phenotype"
- method="POST"
- id="frm-delete-phenotype">
+{%if "group:resource:edit-resource" in privileges
+or "group:resource:delete-resource" in privileges%}
+<div class="row">
+ <form action="#edit-delete-phenotype"
+ method="POST"
+ id="frm-delete-phenotype">
- <input type="hidden" name="species_id" value="{{species.SpeciesId}}" />
- <input type="hidden" name="population_id" value="{{population.Id}}" />
- <input type="hidden" name="dataset_id" value="{{dataset.Id}}" />
- <input type="hidden" name="phenotype_id" value="{{phenotype.Id}}" />
+ <input type="hidden" name="species_id" value="{{species.SpeciesId}}" />
+ <input type="hidden" name="population_id" value="{{population.Id}}" />
+ <input type="hidden" name="dataset_id" value="{{dataset.Id}}" />
+ <input type="hidden" name="phenotype_id" value="{{phenotype.Id}}" />
- <div class="btn-group btn-group-justified">
- <div class="btn-group">
- {%if "group:resource:edit-resource" in privileges%}
- <input type="submit"
- title="Edit the values for the phenotype. This is meant to be used when you need to update only a few values."
- class="btn btn-primary not-implemented"
- value="edit" />
- {%endif%}
- </div>
- <div class="btn-group"></div>
- <div class="btn-group">
- {%if "group:resource:delete-resource" in privileges%}
- <input type="submit"
- title="Delete the entire phenotype. This is useful when you need to change data for most or all of the fields for this phenotype."
- class="btn btn-danger not-implemented"
- value="delete" />
- {%endif%}
- </div>
- </div>
- </form>
- </div>
+ <div class="btn-group btn-group-justified">
+ <div class="btn-group">
+ {%if "group:resource:edit-resource" in privileges%}
+ <input type="submit"
+ title="Edit the values for the phenotype. This is meant to be used when you need to update only a few values."
+ class="btn btn-primary not-implemented"
+ value="edit" />
+ {%endif%}
+ </div>
+ <div class="btn-group"></div>
+ <div class="btn-group">
+ {%if "group:resource:delete-resource" in privileges%}
+ <input type="submit"
+ title="Delete the entire phenotype. This is useful when you need to change data for most or all of the fields for this phenotype."
+ class="btn btn-danger not-implemented"
+ disabled="disabled"
+ value="delete" />
+ {%endif%}
+ </div>
+ </div>
+ </form>
</div>
+{%endif%}
<div class="row">
<div class="panel panel-default">
@@ -90,9 +105,10 @@
<th>#</th>
<th>Sample</th>
<th>Value</th>
- <th>Symbol</th>
- <th>SE</th>
+ {%if has_se%}
+ <th>SE: {{has_se}}</th>
<th>N</th>
+ {%endif%}
</tr>
</thead>
@@ -102,9 +118,10 @@
<td>{{loop.index}}</td>
<td>{{item.StrainName}}</td>
<td>{{item.value}}</td>
- <td>{{item.Symbol or "-"}}</td>
+ {%if has_se%}
<td>{{item.error or "-"}}</td>
<td>{{item.count or "-"}}</td>
+ {%endif%}
</tr>
{%endfor%}
</tbody>