From 493f8fbe747650a4fbac2e0b153ad0074b4f91e4 Mon Sep 17 00:00:00 2001 From: Frederick Muriuki Muriithi Date: Wed, 6 Dec 2023 13:00:53 +0300 Subject: Feature: Upload Samples/Cases Implements the code enabling the upload of the samples/cases to the database. --- qc_app/__init__.py | 2 + qc_app/db_utils.py | 7 +- qc_app/entry.py | 8 +- qc_app/samples.py | 108 ++++++++++++++++++++++++ qc_app/templates/flash_messages.html | 25 ++++++ qc_app/templates/index.html | 35 +++++++- qc_app/templates/samples/select-population.html | 105 +++++++++++++++++++++++ qc_app/templates/samples/upload-samples.html | 107 +++++++++++++++++++++++ 8 files changed, 390 insertions(+), 7 deletions(-) create mode 100644 qc_app/samples.py create mode 100644 qc_app/templates/flash_messages.html create mode 100644 qc_app/templates/samples/select-population.html create mode 100644 qc_app/templates/samples/upload-samples.html diff --git a/qc_app/__init__.py b/qc_app/__init__.py index 4810d45..f2001ab 100644 --- a/qc_app/__init__.py +++ b/qc_app/__init__.py @@ -6,6 +6,7 @@ from flask import Flask from .entry import entrybp from .parse import parsebp +from .samples import samples from .dbinsert import dbinsertbp from .errors import register_error_handlers @@ -30,6 +31,7 @@ def create_app(instance_dir): app.register_blueprint(entrybp, url_prefix="/") app.register_blueprint(parsebp, url_prefix="/parse") app.register_blueprint(dbinsertbp, url_prefix="/dbinsert") + app.register_blueprint(samples, url_prefix="/samples") register_error_handlers(app) return app diff --git a/qc_app/db_utils.py b/qc_app/db_utils.py index a04c5e1..75b6b73 100644 --- a/qc_app/db_utils.py +++ b/qc_app/db_utils.py @@ -2,7 +2,7 @@ import logging import traceback import contextlib -from typing import Tuple, Optional, Iterator +from typing import Any, Tuple, Optional, Iterator, Callable from urllib.parse import urlparse import MySQLdb as mdb @@ -32,3 +32,8 @@ def database_connection(db_url: Optional[str] = None) -> Iterator[mdb.Connection connection.rollback() finally: connection.close() + +def with_db_connection(func: Callable[[mdb.Connection], Any]) -> Any: + """Call `func` with a MySQDdb database connection.""" + with database_connection(app.config["SQL_URI"]) as conn: + return func(conn) diff --git a/qc_app/entry.py b/qc_app/entry.py index abea5ed..bf78037 100644 --- a/qc_app/entry.py +++ b/qc_app/entry.py @@ -14,6 +14,8 @@ from flask import ( render_template, current_app as app) +from .dbinsert import species + entrybp = Blueprint("entry", __name__) def errors(rqst) -> Tuple[str, ...]: @@ -76,14 +78,14 @@ def zip_file_errors(filepath, upload_dir) -> Tuple[str, ...]: def upload_file(): """Enables uploading the files""" if request.method == "GET": - return render_template("index.html") + return render_template("index.html", species = species()) upload_dir = app.config["UPLOAD_FOLDER"] request_errors = errors(request) if request_errors: for error in request_errors: flash(error, "alert-error") - return render_template("index.html"), 400 + return render_template("index.html", species = species()), 400 filename = secure_filename(request.files["qc_text_file"].filename) if not os.path.exists(upload_dir): @@ -96,7 +98,7 @@ def upload_file(): if zip_errors: for error in zip_errors: flash(error, "alert-error") - return render_template("index.html"), 400 + return render_template("index.html", species = species()), 400 return redirect(url_for( "parse.parse", filename=filename, diff --git a/qc_app/samples.py b/qc_app/samples.py new file mode 100644 index 0000000..cc745ca --- /dev/null +++ b/qc_app/samples.py @@ -0,0 +1,108 @@ +"""Code regarding samples""" +import MySQLdb as mdb +from MySQLdb.cursors import DictCursor +from flask import ( + flash, request, url_for, redirect, Blueprint, render_template, current_app as app) + +from .db_utils import with_db_connection +from .dbinsert import species_by_id, groups_by_species + +samples = Blueprint("samples", __name__) + +@samples.route("/upload/species", methods=["POST"]) +def select_species(): + """Select the species.""" + index_page = redirect(url_for("entry.upload_file")) + species_id = request.form.get("species_id") + if bool(species_id): + species_id = int(species_id) + species = species_by_id(species_id) + if bool(species): + return render_template( + "samples/select-population.html", + species=species, + populations=groups_by_species(species_id)) + flash("Invalid species selected!", "alert-error") + flash("You need to select a species", "alert-error") + return index_page + +def save_population(conn: mdb.Connection, population_details: dict) -> int: + """Save the population details to the db.""" + with conn.cursor(cursorclass=DictCursor) as cursor: + cursor.execute("SELECT MAX(Id) AS last_id FROM InbredSet") + new_id = cursor.fetchone()["last_id"] + 1 + cursor.execute( + "INSERT INTO InbredSet(" + "Id, InbredSetId, InbredSetName, Name, SpeciesId, FullName, " + "MenuOrderId, Description" + ") " + "VALUES (" + "%(Id)s, %(InbredSetId)s, %(InbredSetName)s, %(Name)s, " + "%(SpeciesId)s, %(FullName)s, %(MenuOrderId)s, %(Description)s" + ")", + { + "Id": new_id, + "InbredSetId": new_id, + "MenuOrderId": 0, + **population_details + }) + return new_id + +def population_by_id(conn: mdb.Connection, population_id: int) -> dict: + """Get the grouping/population by id.""" + with conn.cursor(cursorclass=DictCursor) as cursor: + cursor.execute("SELECT * FROM InbredSet WHERE InbredSetId=%s", + (population_id,)) + return cursor.fetchone() + +@samples.route("/upload/create-population", methods=["POST"]) +def create_population(): + """Create new grouping/population.""" + species_page = redirect(url_for("samples.select_species"), code=307) + species = species_by_id(request.form.get("species_id")) + pop_name = request.form.get("inbredset_name").strip() + pop_fullname = request.form.get("inbredset_fullname").strip() + + if not bool(species): + flash("Invalid species!", "alert-error error-create-population") + return species_page + if (not bool(pop_name)) or (not bool(pop_fullname)): + flash("You *MUST* provide a grouping/population name", + "alert-error error-create-population") + return species_page + + pop_id = with_db_connection(lambda conn: save_population(conn, { + "SpeciesId": species["SpeciesId"], + "Name": pop_name, + "InbredSetName": pop_fullname, + "FullName": pop_fullname, + "Family": request.form.get("inbredset_family") or None, + "Description": request.form.get("description") or None + })) + flash("Grouping/Population created successfully.", "alert-success") + return render_template( + "samples/upload-samples.html", + species=species, + population=with_db_connection( + lambda conn: population_by_id(conn, pop_id))) + +@samples.route("/upload/select-population", methods=["POST"]) +def select_population(): + """Select from existing groupings/populations.""" + species_page = redirect(url_for("samples.select_species"), code=307) + species = species_by_id(request.form.get("species_id")) + pop_id = int(request.form.get("inbredset_id")) + population = with_db_connection(lambda conn: population_by_id(conn, pop_id)) + + if not bool(species): + flash("Invalid species!", "alert-error error-select-population") + return species_page + + if not bool(population): + flash("Invalid grouping/population!", + "alert-error error-select-population") + return species_page + + return render_template("samples/upload-samples.html", + species=species, + population=population) diff --git a/qc_app/templates/flash_messages.html b/qc_app/templates/flash_messages.html new file mode 100644 index 0000000..b7af178 --- /dev/null +++ b/qc_app/templates/flash_messages.html @@ -0,0 +1,25 @@ +{%macro flash_all_messages()%} +{%with messages = get_flashed_messages(with_categories=true)%} +{%if messages:%} + +{%endif%} +{%endwith%} +{%endmacro%} + +{%macro flash_messages(filter_class)%} +{%with messages = get_flashed_messages(with_categories=true)%} +{%if messages:%} + +{%endif%} +{%endwith%} +{%endmacro%} diff --git a/qc_app/templates/index.html b/qc_app/templates/index.html index 2db6048..e534bcb 100644 --- a/qc_app/templates/index.html +++ b/qc_app/templates/index.html @@ -1,9 +1,11 @@ {%extends "base.html"%} -{%block title%}Upload File{%endblock%} +{%block title%}Data Upload{%endblock%} {%block contents%} -

upload file

+

data upload

+ +

Expression Data

This application assumes that you are familiar with the basics of data @@ -26,7 +28,7 @@

- upload file + upload expression data {%with messages = get_flashed_messages(with_categories=True) %} {%if messages %}
@@ -81,6 +83,33 @@
+ +

samples/cases

+ +
+

For the expression data above, you need the samples/cases in your file to + already exist in the GeneNetwork database. If there are any samples that do + not already exist the upload of the expression data will fail.

+

This section gives you the opportunity to upload any missing samples

+
+ +
+ upload samples +
+ + +
+ +
+ +
+
+ {%endblock%} diff --git a/qc_app/templates/samples/select-population.html b/qc_app/templates/samples/select-population.html new file mode 100644 index 0000000..24decb4 --- /dev/null +++ b/qc_app/templates/samples/select-population.html @@ -0,0 +1,105 @@ +{%extends "base.html"%} +{%from "flash_messages.html" import flash_messages%} + +{%block title%}Select Grouping/Population{%endblock%} + +{%block contents%} +

Select grouping/population

+ +
+

We organise the samples/cases/strains in a hierarchichal form, starting + with species at the very top. Under species, we have a + grouping in terms of the relevant population + (e.g. Inbred populations, cell tissue, etc.)

+
+ +
+ +
+ select grouping/population + {{flash_messages("error-select-population")}} + + +
+ + +
+ +
+ +
+
+ +

OR

+ +
+ create new grouping/population + {{flash_messages("error-create-population")}} + + +
+ mandatory + + + + + +
+
+ Optional + + + + + + + + + +
+ +
+ +
+
+ +{%endblock%} + + +{%block javascript%} +{%endblock%} diff --git a/qc_app/templates/samples/upload-samples.html b/qc_app/templates/samples/upload-samples.html new file mode 100644 index 0000000..b19e38c --- /dev/null +++ b/qc_app/templates/samples/upload-samples.html @@ -0,0 +1,107 @@ +{%extends "base.html"%} +{%from "flash_messages.html" import flash_messages%} + +{%block title%}Upload Samples{%endblock%} + +{%block css%} + +{%endblock%} + +{%block contents%} +

upload samples

+ +{{flash_messages("alert-success")}} + +

You can now upload a character-separated value (CSV) file that contains + details about your samples. The CSV file should have the following fields: +

+
Name
+
The primary name for the sample
+ +
Name2
+
A secondary name for the sample. This can simply be the same as + Name above. This field MUST contain a + value.
+ +
Symbol
+
A symbol for the sample. Can be an empty field.
+ +
Alias
+
An alias for the sample. Can be an empty field.
+
+

+ +
+ upload samples +
+ + + +
+ +
+ + + +
+ +
+ + + + +
+ +
+ + + +
+ +
+ + +
+ +
+ +
+
+ +{%endblock%} + + +{%block javascript%} +{%endblock%} -- cgit v1.2.3