aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--qc_app/static/css/styles.css5
-rw-r--r--qc_app/templates/rqtl2/upload-rqtl2-bundle.html44
-rw-r--r--qc_app/upload/rqtl2.py56
-rw-r--r--r_qtl/r_qtl2.py24
4 files changed, 129 insertions, 0 deletions
diff --git a/qc_app/static/css/styles.css b/qc_app/static/css/styles.css
index 8cfd15f..f4112d1 100644
--- a/qc_app/static/css/styles.css
+++ b/qc_app/static/css/styles.css
@@ -156,6 +156,11 @@ fieldset {
grid-column: 2 / 3;
}
+.form-input-help p {
+ margin-top: 0;
+ margin-bottom: 0.6em;
+}
+
input[disabled="true"],input[disabled="disabled"] {
border-color: #878787;
background-color: #A9A9A9;
diff --git a/qc_app/templates/rqtl2/upload-rqtl2-bundle.html b/qc_app/templates/rqtl2/upload-rqtl2-bundle.html
new file mode 100644
index 0000000..6491e6b
--- /dev/null
+++ b/qc_app/templates/rqtl2/upload-rqtl2-bundle.html
@@ -0,0 +1,44 @@
+{%extends "base.html"%}
+{%from "flash_messages.html" import flash_messages%}
+
+{%block title%}Upload R/qtl2 Bundle{%endblock%}
+
+{%block contents%}
+<h2 class="heading">Upload R/qtl2 Bundle</h2>
+
+<form id="frm-upload-rqtl2-bundle"
+ action="{{url_for('upload.rqtl2.upload_rqtl2_bundle',
+ species_id=species.SpeciesId,
+ population_id=population.InbredSetId)}}"
+ method="POST"
+ enctype="multipart/form-data">
+ <input type="hidden" name="species_id" value="{{species.SpeciesId}}" />
+ <input type="hidden" name="population_id"
+ value="{{population.InbredSetId}}" />
+
+ {{flash_messages("error-rqtl2")}}
+
+ <fieldset>
+ <legend>file upload</legend>
+ <label for="file:rqtl2-bundle">R/qtl2 bundle</label>
+ <input type="file" id="file:rqtl2-bundle" name="rqtl2_bundle"
+ accept="application/zip, .zip"
+ required="required" />
+ <span class="form-input-help"><p>Provide a valid R/qtl2 zip file here. In
+ particular, ensure your zip bundle contains exactly one control file and
+ the corresponding files mentioned in the control file.</p>
+ <p>The control file can be either a YAML or JSON file. <em>ALL</em> other
+ data files in the zip bundle should be CSV files.</p>
+ <p>See the
+ <a href="https://kbroman.org/qtl2/assets/vignettes/input_files.html"
+ target="_blank">
+ R/qtl2 file format specifications</a> for more details.</p></span>
+ </fieldset>
+ <fieldset>
+ <input type="submit"
+ value="upload R/qtl2 bundle"
+ class="btn btn-main form-col-2" />
+ </fieldset>
+</form>
+
+{%endblock%}
diff --git a/qc_app/upload/rqtl2.py b/qc_app/upload/rqtl2.py
index 681e54c..53e7f3f 100644
--- a/qc_app/upload/rqtl2.py
+++ b/qc_app/upload/rqtl2.py
@@ -1,4 +1,6 @@
"""Module to handle uploading of R/qtl2 bundles."""
+from pathlib import Path
+from zipfile import ZipFile, is_zipfile
from flask import (
flash,
@@ -9,6 +11,10 @@ from flask import (
render_template,
current_app as app)
+from r_qtl import r_qtl2
+from r_qtl.errors import InvalidFormat
+
+from qc_app.files import save_file
from qc_app.dbinsert import species as all_species
from qc_app.db_utils import with_db_connection, database_connection
from qc_app.db import (
@@ -96,3 +102,53 @@ def create_population(species_id: int):
population_id=new_population["population_id"],
pgsrc="create-population"),
code=307)
+
+@rqtl2.route(("/upload/species/<int:species_id>/population/<int:population_id>"
+ "/rqtl2-bundle"),
+ methods=["GET", "POST"])
+def upload_rqtl2_bundle(species_id: int, population_id: int):
+ """Allow upload of R/qtl2 bundle."""
+ this_page_with_errors = redirect(url_for("upload.rqtl2.upload_rqtl2_bundle",
+ species_id=species_id,
+ population_id=population_id,
+ pgsrc="error"),
+ code=307)
+
+ with database_connection(app.config["SQL_URI"]) as conn:
+ species = species_by_id(conn, species_id)
+ population = population_by_species_and_id(
+ conn, species["SpeciesId"], population_id)
+ if not bool(species):
+ flash("Invalid species!", "alert-error error-rqtl2")
+ return redirect(url_for("upload.rqtl2.select_species"))
+ if not bool(population):
+ flash("Invalid Population!", "alert-error error-rqtl2")
+ return redirect(
+ url_for("upload.rqtl2.select_population", pgsrc="error"),
+ code=307)
+ if request.method == "GET" or (
+ request.method == "POST"
+ and bool(request.args.get("pgsrc"))):
+ return render_template("rqtl2/upload-rqtl2-bundle.html",
+ species=species,
+ population=population)
+
+ the_file = save_file(
+ request.files.get("rqtl2_bundle"), Path(app.config["UPLOAD_FOLDER"]))
+ if not bool(the_file):
+ flash("Please provide a valid R/qtl2 zip bundle.",
+ "alert-error alert-danger error-rqtl2")
+ return this_page_with_errors
+
+ if not is_zipfile(the_file):
+ flash("Invalid file! Expected a zip file.",
+ "alert-error alert-danger error-rqtl2")
+ return this_page_with_errors
+
+ try:
+ with ZipFile(the_file, "r") as zfile:
+ r_qtl2.validate_bundle(zfile)
+ return "WOULD PROCESS THE BUNDLE..."
+ except InvalidFormat as invf:
+ flash("".join(invf.args), "alert-error alert-danger error-rqtl2")
+ return this_page_with_errors
diff --git a/r_qtl/r_qtl2.py b/r_qtl/r_qtl2.py
index 2c1e162..a8958a0 100644
--- a/r_qtl/r_qtl2.py
+++ b/r_qtl/r_qtl2.py
@@ -242,3 +242,27 @@ def sex_information(zfile: ZipFile, cdata: dict) -> Iterator[dict]:
yield {
key: thread_op(value, partial(replace_sex_info, cdata=cdata))
for key, value in row.items() if key not in ci_fields}
+
+def validate_bundle(zfile: ZipFile):
+ """Ensure the R/qtl2 bundle is valid."""
+ cdata = control_data(zfile)
+ def __member_exists_p__(zfile, member):
+ if isinstance(member, str):
+ zfile.getinfo(member)
+ else:
+ for inner in member:
+ zfile.getinfo(inner)
+
+ try:
+ for member in (key for key in cdata.keys() if key in (
+ "geno", "founder_geno", "pheno", "covar", "phenocovar", "gmap",
+ "pmap")):
+ __member_exists_p__(zfile, cdata[member])
+
+ if "file" in cdata.get("sex", {}):
+ __member_exists_p__(zfile, cdata["sex"]["file"])
+
+ if "file" in cdata.get("cross_info", {}):
+ __member_exists_p__(zfile, cdata["cross_info"]["file"])
+ except KeyError as kerr:
+ raise InvalidFormat(*kerr.args) from kerr