From 6326357250711650bc8b4c0b90f8c0b94d72c39e Mon Sep 17 00:00:00 2001 From: Frederick Muriuki Muriithi Date: Fri, 29 Mar 2024 13:14:14 +0300 Subject: Add UI to select/create tissue. --- qc_app/db/tissues.py | 31 +++++- qc_app/templates/rqtl2/create-tissue-success.html | 104 +++++++++++++++++++ .../templates/rqtl2/select-probeset-study-id.html | 20 +--- qc_app/templates/rqtl2/select-tissue.html | 112 +++++++++++++++++++++ qc_app/upload/rqtl2.py | 104 ++++++++++++++++++- 5 files changed, 349 insertions(+), 22 deletions(-) create mode 100644 qc_app/templates/rqtl2/create-tissue-success.html create mode 100644 qc_app/templates/rqtl2/select-tissue.html diff --git a/qc_app/db/tissues.py b/qc_app/db/tissues.py index ebf24fd..062e824 100644 --- a/qc_app/db/tissues.py +++ b/qc_app/db/tissues.py @@ -1,5 +1,5 @@ """Handle db interactions for tissue.""" -from typing import Optional +from typing import Union, Optional import MySQLdb as mdb from MySQLdb.cursors import DictCursor @@ -10,6 +10,7 @@ def all_tissues(conn: mdb.Connection) -> tuple[dict, ...]: cursor.execute("SELECT * FROM Tissue ORDER BY TissueName") return tuple(dict(row) for row in cursor.fetchall()) + def tissue_by_id(conn: mdb.Connection, tissueid: int) -> Optional[dict]: """Retrieve a tissue by its ID""" with conn.cursor(cursorclass=DictCursor) as cursor: @@ -20,3 +21,31 @@ def tissue_by_id(conn: mdb.Connection, tissueid: int) -> Optional[dict]: return dict(result) return None + + +def create_new_tissue( + conn: mdb.Connection, + name: str, + shortname: str, + birnlexid: Optional[str] = None, + birnlexname: Optional[str] = None +) -> dict[str, Union[int, str, None]]: + """Add a new tissue, organ or biological material to the database.""" + with conn.cursor() as cursor: + cursor.execute( + "INSERT INTO " + "Tissue(TissueName, Name, Short_Name, BIRN_lex_ID, BIRN_lex_Name) " + "VALUES (%s, %s, %s, %s, %s)", + (name, name, shortname, birnlexid, birnlexname)) + tissueid = cursor.lastrowid + cursor.execute("UPDATE Tissue SET TissueId=%s WHERE Id=%s", + (tissueid, tissueid)) + return { + "Id": tissueid, + "TissueId": tissueid, + "TissueName": name, + "Name": name, + "Short_Name": shortname, + "BIRN_lex_ID": birnlexid, + "BIRN_lex_Name": birnlexname + } diff --git a/qc_app/templates/rqtl2/create-tissue-success.html b/qc_app/templates/rqtl2/create-tissue-success.html new file mode 100644 index 0000000..ea05624 --- /dev/null +++ b/qc_app/templates/rqtl2/create-tissue-success.html @@ -0,0 +1,104 @@ +{%extends "base.html"%} +{%from "flash_messages.html" import flash_all_messages%} + +{%block title%}Upload R/qtl2 Bundle{%endblock%} + +{%block contents%} +

Select Tissue

+ +
+

You have successfully added a new tissue, organ or biological material with + the following details:

+
+ +{{flash_all_messages()}} + +
+ Create Tissue + + + + + + + +
+ + +
+ +
+ + +
+ + {%if tissue.BIRN_lex_ID%} +
+ + +
+ {%endif%} + + {%if tissue.BIRN_lex_Name%} +
+ + +
+ {%endif%} +
+ +
+ +
+ + + + + + + + +
+ +

+ OR +

+ +
+ + + + + + + +
+ +
+ +{%endblock%} diff --git a/qc_app/templates/rqtl2/select-probeset-study-id.html b/qc_app/templates/rqtl2/select-probeset-study-id.html index 3bbb94a..23bbbf0 100644 --- a/qc_app/templates/rqtl2/select-probeset-study-id.html +++ b/qc_app/templates/rqtl2/select-probeset-study-id.html @@ -26,6 +26,7 @@ +
@@ -71,6 +72,7 @@ +
@@ -89,24 +91,6 @@
-
- - - - Select the Tissue to which the data is concerned. - -
-
Tissue + +
+

The data you are uploading concerns a tissue, cell, organ, or other + biological material used in an experiment.

+

Select the appropriate biological material below

+
+ +{%if tissues | length > 0%} +
+ Select from existing ProbeSet datasets + {{flash_messages("error-select-tissue")}} + + + + + + +
+ + + + Select from existing biological material. +
+ +
+ +
+
+ +

OR

+{%endif%} + +
+

If you cannot find the biological material in the drop-down above, add it + to the system below.

+
+ +
+ Add new tissue, organ or biological material + {{flash_messages("error-create-tissue")}} + + + + + + +
+ + + + + A name to identify the tissue, organ or biological material. + +
+ +
+ + + + + Provide a short name for the tissue, organ or biological material used in + the experiment. + +
+ +
+ +
+
+ +{%endblock%} diff --git a/qc_app/upload/rqtl2.py b/qc_app/upload/rqtl2.py index c05f675..7a53756 100644 --- a/qc_app/upload/rqtl2.py +++ b/qc_app/upload/rqtl2.py @@ -28,9 +28,9 @@ from qc_app.files import save_file, fullpath from qc_app.dbinsert import species as all_species from qc_app.db_utils import with_db_connection, database_connection -from qc_app.db.tissues import all_tissues, tissue_by_id from qc_app.db.platforms import platform_by_id, platforms_by_species from qc_app.db.averaging import averaging_methods, averaging_method_by_id +from qc_app.db.tissues import all_tissues, tissue_by_id, create_new_tissue from qc_app.db import ( species_by_id, save_population, @@ -65,6 +65,7 @@ def select_species(): flash("Invalid species or no species selected!", "alert-error error-rqtl2") return redirect(url_for("upload.rqtl2.select_species")) + @rqtl2.route("/upload/species//select-population", methods=["GET", "POST"]) def select_population(species_id: int): @@ -93,6 +94,7 @@ def select_population(species_id: int): species_id=species["SpeciesId"], population_id=population["InbredSetId"])) + @rqtl2.route("/upload/species//create-population", methods=["POST"]) def create_population(species_id: int): @@ -129,9 +131,11 @@ def create_population(species_id: int): pgsrc="create-population"), code=307) + class __RequestError__(Exception): #pylint: disable=[invalid-name] """Internal class to avoid pylint's `too-many-return-statements` error.""" + @rqtl2.route(("/upload/species//population/" "/rqtl2-bundle"), methods=["GET", "POST"]) @@ -197,6 +201,7 @@ def upload_rqtl2_bundle(species_id: int, population_id: int): return redirect(url_for( "upload.rqtl2.rqtl2_bundle_qc_status", jobid=jobid)) + @rqtl2.route("/upload/species/rqtl2-bundle/qc-status/", methods=["GET", "POST"]) def rqtl2_bundle_qc_status(jobid: UUID): @@ -255,15 +260,18 @@ def rqtl2_bundle_qc_status(jobid: UUID): except jobs.JobNotFound: return render_template("rqtl2/no-such-job.html", jobid=jobid) + def form_errors(formargs, *errorcheckers) -> Iterable[tuple[str, Response]]: """Retrieve all errors from the form inputs""" return (checker(formargs) for checker in errorcheckers) + def redirect_on_error(flaskroute, **kwargs): """Utility to redirect on error""" return redirect(url_for(flaskroute, **kwargs, pgsrc="error"), code=(307 if request.method == "POST" else 302)) + def check_species(conn: mdb.Connection, formargs: dict) -> Optional[ tuple[str, Response]]: """ @@ -280,6 +288,7 @@ def check_species(conn: mdb.Connection, formargs: dict) -> Optional[ return None + def check_population(conn: mdb.Connection, formargs: dict, species_id) -> Optional[tuple[str, Response]]: @@ -299,6 +308,7 @@ def check_population(conn: mdb.Connection, return None + def check_r_qtl2_bundle(formargs: dict, species_id, population_id) -> Optional[tuple[str, Response]]: @@ -315,6 +325,7 @@ def check_r_qtl2_bundle(formargs: dict, return None + def check_geno_dataset(formargs: dict, species_id, population_id) -> Optional[tuple[str, Response]]: @@ -328,6 +339,7 @@ def check_geno_dataset(formargs: dict, return None + def check_probe_study(formargs: dict, species_id, population_id) -> Optional[tuple[str, Response]]: @@ -340,6 +352,7 @@ def check_probe_study(formargs: dict, return None + def check_probe_dataset(formargs: dict, species_id, population_id) -> Optional[tuple[str, Response]]: @@ -352,6 +365,7 @@ def check_probe_dataset(formargs: dict, return None + def with_errors(conn: mdb.Connection, endpointthunk: Callable, *checkerstrings): """Run 'endpointthunk' with error checking.""" formargs = {**dict(request.args), **dict(request.form)} @@ -378,6 +392,7 @@ def with_errors(conn: mdb.Connection, endpointthunk: Callable, *checkerstrings): return endpointthunk() + @rqtl2.route(("/upload/species//population/" "/rqtl2-bundle/select-geno-dataset"), methods=["POST"]) @@ -406,6 +421,7 @@ def select_geno_dataset(species_id: int, population_id: int): return with_errors( conn, __thunk__, "species", "population", "rqtl2_bundle_file", "geno_dataset") + @rqtl2.route(("/upload/species//population/" "/rqtl2-bundle/create-geno-dataset"), methods=["POST"]) @@ -457,6 +473,71 @@ def create_geno_dataset(species_id: int, population_id: int): return with_errors(conn, __thunk__, "species", "population", "rqtl2_bundle_file") + +@rqtl2.route(("/upload/species//population/" + "/rqtl2-bundle/select-tissue"), + methods=["POST"]) +def select_tissue(species_id: int, population_id: int): + """Select from existing tissues.""" + with database_connection(app.config["SQL_URI"]) as conn: + def __thunk__(): + if not bool(request.form.get("tissueid", "").strip()): + flash("Invalid tissue selection!", + "alert-error error-select-tissue error-rqtl2") + + return redirect(url_for("upload.rqtl2.select_dataset_info", + species_id=species_id, + population_id=population_id, + pgsrc="upload.rqtl2.select_geno_dataset"), + code=307) + + return with_errors( + conn, __thunk__, "species", "population", "rqtl2_bundle_file", "geno_dataset") + +@rqtl2.route(("/upload/species//population/" + "/rqtl2-bundle/create-tissue"), + methods=["POST"]) +def create_tissue(species_id: int, population_id: int): + """Add new tissue, organ or biological material to the system.""" + form = request.form + datasetinfopage = redirect( + url_for("upload.rqtl2.select_dataset_info", + species_id=species_id, + population_id=population_id, + pgsrc="upload.rqtl2.select_geno_dataset"), + code=307) + with database_connection(app.config["SQL_URI"]) as conn: + tissuename = form.get("tissuename", "").strip() + tissueshortname = form.get("tissueshortname", "").strip() + if not bool(tissuename): + flash("Organ/Tissue name MUST be provided.", + "alert-error error-create-tissue error-rqtl2") + return datasetinfopage + + if not bool(tissueshortname): + flash("Organ/Tissue short name MUST be provided.", + "alert-error error-create-tissue error-rqtl2") + return datasetinfopage + + try: + tissue = create_new_tissue(conn, tissuename, tissueshortname) + flash("Tissue created successfully!", "alert-success") + return render_template( + "rqtl2/create-tissue-success.html", + species=species_by_id(conn, species_id), + population=population_by_species_and_id( + conn, species_id, population_id), + rqtl2_bundle_file=request.form["rqtl2_bundle_file"], + geno_dataset=geno_dataset_by_id( + conn, + int(request.form["geno-dataset-id"])), + tissue=tissue) + except mdb.IntegrityError as _ierr: + flash("Tissue/Organ with that short name already exists!", + "alert-error error-create-tissue error-rqtl2") + return datasetinfopage + + @rqtl2.route(("/upload/species//population/" "/rqtl2-bundle/select-probeset-study"), methods=["POST"]) @@ -477,6 +558,7 @@ def select_probeset_study(species_id: int, population_id: int): "rqtl2_bundle_file", "geno_dataset", "probeset_study") + @rqtl2.route(("/upload/species//population/" "/rqtl2-bundle/select-probeset-dataset"), methods=["POST"]) @@ -498,6 +580,7 @@ def select_probeset_dataset(species_id: int, population_id: int): "rqtl2_bundle_file", "geno_dataset", "probeset_study", "probeset_dataset") + @rqtl2.route(("/upload/species//population/" "/rqtl2-bundle/create-probeset-study"), methods=["POST"]) @@ -540,6 +623,7 @@ def create_probeset_study(species_id: int, population_id: int): return with_errors(conn, __thunk__, "species", "population", "rqtl2_bundle_file", "geno_dataset") + @rqtl2.route(("/upload/species//population/" "/rqtl2-bundle/create-probeset-dataset"), methods=["POST"]) @@ -597,6 +681,7 @@ def create_probeset_dataset(species_id: int, population_id: int):#pylint: disabl "rqtl2_bundle_file", "geno_dataset", "probeset_study") + @rqtl2.route(("/upload/species//population/" "/rqtl2-bundle/dataset-info"), methods=["POST"]) @@ -625,6 +710,19 @@ def select_dataset_info(species_id: int, population_id: int): geno_dataset = geno_dataset_by_id(conn, int(form["geno-dataset-id"])) if "pheno" in cdata and not bool(form.get("probe-study-id")): + tissue = tissue_by_id(conn, form.get("tissueid", "").strip()) + if not bool(tissue): + return render_template( + "rqtl2/select-tissue.html", + species=species, + population=population, + rqtl2_bundle_file=thefile.name, + geno_dataset=geno_dataset, + studies=probeset_studies_by_species_and_population( + conn, species_id, population_id), + platforms=platforms_by_species(conn, species_id), + tissues=all_tissues(conn)) + return render_template( "rqtl2/select-probeset-study-id.html", species=species, @@ -634,7 +732,7 @@ def select_dataset_info(species_id: int, population_id: int): studies=probeset_studies_by_species_and_population( conn, species_id, population_id), platforms=platforms_by_species(conn, species_id), - tissues=all_tissues(conn)) + tissue=tissue) probeset_study = probeset_study_by_id( conn, int(form["probe-study-id"])) @@ -666,7 +764,6 @@ def select_dataset_info(species_id: int, population_id: int): "rqtl2_bundle_file") - @rqtl2.route(("/upload/species//population/" "/rqtl2-bundle/confirm-bundle-details"), methods=["POST"]) @@ -719,6 +816,7 @@ def confirm_bundle_details(species_id: int, population_id: int): "rqtl2_bundle_file", "geno_dataset", "probeset_study", "probeset_dataset") + @rqtl2.route("/status/") def rqtl2_processing_status(jobid: UUID): """Retrieve the status of the job processing the uploaded R/qtl2 bundle.""" -- cgit v1.2.3