diff options
-rw-r--r-- | qc_app/static/css/styles.css | 5 | ||||
-rw-r--r-- | qc_app/templates/rqtl2/upload-rqtl2-bundle.html | 44 | ||||
-rw-r--r-- | qc_app/upload/rqtl2.py | 56 | ||||
-rw-r--r-- | r_qtl/r_qtl2.py | 24 |
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 |