diff options
Diffstat (limited to 'uploader')
-rw-r--r-- | uploader/files/functions.py | 11 | ||||
-rw-r--r-- | uploader/files/views.py | 43 | ||||
-rw-r--r-- | uploader/oauth2/client.py | 3 | ||||
-rw-r--r-- | uploader/phenotypes/views.py | 253 | ||||
-rw-r--r-- | uploader/static/css/styles.css | 14 | ||||
-rw-r--r-- | uploader/templates/base.html | 2 | ||||
-rw-r--r-- | uploader/templates/phenotypes/add-phenotypes-raw-files.html | 107 | ||||
-rw-r--r-- | uploader/templates/phenotypes/job-status.html | 26 | ||||
-rw-r--r-- | uploader/templates/phenotypes/macro-display-preview-table.html | 4 | ||||
-rw-r--r-- | uploader/templates/phenotypes/macro-display-resumable-elements.html | 60 | ||||
-rw-r--r-- | uploader/templates/phenotypes/review-job-data.html | 101 | ||||
-rw-r--r-- | uploader/templates/phenotypes/view-dataset.html | 3 | ||||
-rw-r--r-- | uploader/templates/phenotypes/view-phenotype.html | 89 |
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 “<strong>{{dataset.FullName}}</strong>” dataset from the + “<strong>{{population.FullName}}</strong>” population of the + species “<strong>{{species.SpeciesName}} ({{species.FullName}})</strong>” + 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> |